Compare commits

...
80 Commits
Author SHA1 Message Date
Joe Hirschfeld eefd80005e More modifications, back to basics 2020-02-06 22:16:19 -08:00
Joe Hirschfeld 3a2cdfd7ea Refactor some stuff around
There are a _lot_ of changes, including a ton which segment dependencies
better. So thats cool
2020-02-03 23:42:25 -08:00
Joe Hirschfeld d0089c1c0c some stuff 2019-12-24 16:53:13 -08:00
Joe Hirschfeld aa1d9b3d71 [api] Add additional data field to the vote 2019-12-08 12:59:09 -08:00
Joe Hirschfeld c250048bf7 Remove localTimestamp field from Vote 2019-10-11 21:49:35 -07:00
Joe Hirschfeld 1db78a5bea Create new api module
Creates a new API module with stable api interfaces. Adds some unit
tests to the vote test, updates unit testing framework across the board.
2019-10-11 21:35:58 -07:00
Joe Hirschfeld 2ad0fa118f Update repositories in build.gradle 2019-10-11 20:43:05 -07:00
Joseph Hirschfeld 38afe59db0 Remove discord support link 2019-10-02 21:18:30 -07:00
Joseph Hirschfeld b173930c14 Add some much needed newlines! 2019-06-29 22:50:54 -07:00
Joseph Hirschfeld 61c4529df6 Update readme 2019-06-29 22:49:58 -07:00
Joseph Hirschfeld dd48af5b50 Move docs to github wiki page 2019-06-29 22:33:41 -07:00
Glare bc3c667d2a Fix a typo in the FAQ (#148) 2019-06-28 22:27:33 -07:00
Joseph Hirschfeld 1041bdb6b7 Add paypal donation link 2019-06-22 10:46:29 -04:00
Joseph Hirschfeld fc7a04547c Use bukkit ChatColor, not md5 ChatColor 2019-06-19 01:24:04 -04:00
Joseph Hirschfeld dd2a1c4ed5 Add /(p)testvote command 2019-06-19 01:20:07 -04:00
Joseph Hirschfeld c7667abf24 Remove bad singleton pattern use 2019-06-19 00:26:34 -04:00
Joseph Hirschfeld df484523c2 Add /nv(p)reload to all platforms
Also extract all existing platforms into a more modular command
interface
2019-06-19 00:23:14 -04:00
Joseph Hirschfeld 81fca5b218 Only expose the remote address string to handlers 2019-06-18 23:29:30 -04:00
Joseph Hirschfeld c520fd6650 Assume new cache when cachefile is empty 2019-06-17 10:33:46 -04:00
Andrew Steinborn e71ff635e3 Fix ConcurrentModificationException whilst sweeping votes (#141)
Fixes #139
2019-05-31 14:19:48 -04:00
Andrew Steinborn 4d52ef42e4 Fix plugin initialization on Sponge. (#142)
NOTE: In the future, should we support a platform that doesn't provide
Guava, we should simply remove the dependency from nuvotifier-common.
2019-05-31 13:25:16 -04:00
Joseph Hirschfeld c21d18594d Bump version to 3.0.0-SNAPSHOT 2019-04-27 00:21:50 -04:00
Joseph Hirschfeld 9944bb7077 Release 2.5.2 2019-04-27 00:20:31 -04:00
Joseph Hirschfeld 7d43b7399f Do not relocate google code from sponge
Fixes an issue where our shaded google code collided with Sponge,
causing mass sadness. Caused due to b745236
2019-04-27 00:17:04 -04:00
Joseph Hirschfeld 5d797065cb Update version to 2.5.2-SNAPSHOT 2019-04-25 23:58:10 -04:00
Joseph Hirschfeld 43c8489574 Release 2.5.1 2019-04-25 23:55:53 -04:00
Joseph Hirschfeld 348dbca900 Trim whitespace on either side of b64 encoded keys 2019-04-25 23:52:47 -04:00
Joseph Hirschfeld 395c63a80b update dev repo 2019-03-29 23:35:14 -04:00
Joseph Hirschfeld 99afd0ca71 rework env 2019-03-23 16:39:25 -04:00
Joseph Hirschfeld 2243be4954 missing env 2019-03-23 16:37:31 -04:00
Joseph Hirschfeld 8a73b0bf9c fix 2019-03-23 16:37:09 -04:00
Joseph Hirschfeld 595b7ed75f use ibj.io nexus 2019-03-23 16:16:43 -04:00
Joseph Hirschfeld 759b7a886b use openjdk 12 2019-03-23 16:14:14 -04:00
Joseph Hirschfeld eb68085746 fix some stuff in the jenkinsfile 2019-03-23 15:48:44 -04:00
Joseph Hirschfeld 16b093e4d6 psh this should be safe 2019-03-23 15:25:06 -04:00
Joseph Hirschfeld a2848e43d0 remove drone 2019-03-23 15:19:06 -04:00
Joseph Hirschfeld 1bfa28af48 Add jenkinsfile 2019-03-23 15:18:59 -04:00
Joseph Hirschfeld b745236131 Changes to packaging to fix issues with thermos 2019-03-11 16:50:15 -04:00
Joseph Hirschfeld 4330d41286 Fix FAQ formatting 2019-03-11 16:37:07 -04:00
Andrew Steinborn 75228868f8 Fix annotation processor-based implementations (Sponge and Velocity) (#122)
Fixes #121.
2019-02-05 18:51:07 -05:00
Joseph Hirschfeld 671c6e17a9 Do not try to use format args for displaying bind error 2019-01-24 13:44:52 -05:00
Joseph Hirschfeld c3030d56e8 Add FAQ 2019-01-22 17:16:14 -05:00
Joseph Hirschfeld 9fe4d6b9c3 change authorship 2019-01-22 16:06:32 -05:00
Joseph Hirschfeld 99dbaa703a Default to debug mode
People are incapable of turning on debug mode. This should help.
2019-01-22 15:54:31 -05:00
Joseph Hirschfeld 0fa42b44cc Add more debug from RSAIO decoding 2019-01-17 12:24:30 -05:00
Joseph Hirschfeld 7d75bce02c update netty to 4.1.32.Final 2019-01-04 13:59:53 -05:00
Joseph Hirschfeld 60923598b9 Bump to 2.5.1-SNAPSHOT 2019-01-03 14:40:06 -05:00
Joseph Hirschfeld 0c6dc99bcc Release 2.5.0 2019-01-03 14:37:51 -05:00
Joseph Hirschfeld f5776dd1cb Add missing file 2019-01-03 14:36:03 -05:00
Joseph Hirschfeld 7134c12826 Migrate forward sink to common, add tests
Fix an issue where decoding multiple votes in the compressed format
would result in an exception. Adds tests to ensure this doesn't happen
anymore.

Fixes #115
2019-01-03 14:26:54 -05:00
Joseph Hirschfeld 8cc93daf70 Create test dev environment 2018-12-21 01:02:53 -05:00
McModknower 98dfc345e9 Use 'new File(parent, name)' instead of 'new File(parent + "/"+name)' (#113)
* Change 'new File(getDataFolder() + "/...")' to use the 2 argument constructor of File instead.

* Change 'new File(folder + "/...")' to use the 2 argument constructor of File instead
2018-12-14 11:25:46 -05:00
Joseph Hirschfeld 590b7618f3 Push experimental support for bulk vote forwarding 2018-12-10 12:53:32 -05:00
Joseph Hirschfeld 4cd2ce55fc Add ServerFilter, behavior for when online forwarding is used 2018-12-10 12:41:39 -05:00
McModknower 9fe98a92a9 Do not use "/" as a path seperator (#110)
Instead use two argument invocation of new File(). Adds compatibility with Windows hosts.

Co-Authored-By: McModknower <teetoll@t-online.de>
2018-11-09 12:47:07 -05:00
Andrew Steinborn c72972c286 Fix issues in #102 (#103)
* Port to the latest Velocity API

* Actually register channels
2018-09-19 16:06:47 -04:00
Andrew Steinborn 92a122eaf8 Port to the latest Velocity API (#102) 2018-09-19 15:42:11 -04:00
Joseph Hirschfeld 67117b38e3 Add pluginMessage vote cache throttling. 2018-09-11 22:14:46 -04:00
Joseph Hirschfeld 9255062aff Fix configuration defaults, add memory cache ttl 2018-09-11 21:44:21 -04:00
Joseph Hirschfeld fe45e48447 We are 2.4.2 now. Also, fix plugin versions 2018-09-10 14:07:01 -04:00
Joseph Hirschfeld 0391e9e4bf Gotta be a snapshot... 2018-09-10 14:03:23 -04:00
Joseph Hirschfeld 59fa4857e3 Remove slf4j from non slf4j ports, use LoggingAdapter 2018-09-10 13:58:35 -04:00
Andrew Steinborn 4d663737c1 Fixed reference to capitialized channel name (#98) 2018-09-04 09:42:13 -04:00
Joseph Hirschfeld 8380ffce9a Release 2.4.1 2018-09-03 11:33:14 -04:00
Joseph Hirschfeld b9e648bba8 Spigot 1.13.1 wants these lowercase... 2018-09-03 11:31:42 -04:00
Joseph Hirschfeld c34551e630 Bump version to 2.4.1-SNAPSHOT 2018-09-02 22:21:19 -04:00
Joseph Hirschfeld d931cc2464 2.4.0 Release 2018-09-02 22:20:44 -04:00
Andrew Steinborn 07f81eb781 Velocity support (#94)
* Started porting to Velocity

* Heading down into the rabbit hole.

This begins a refactor of NuVotifier to more easily support other platforms
in the future. Much of this work is required to ensure feature-for-feature
compatibility between NuVotifier for BungeeCord and NuVotifier for Velocity.

Doesn't compile!

* Fixes the compile, adds SLF4J stuff.

Far from done!

* Independent Votifier server bootstrap

* Use independent Votifier server bootstrap across all platforms

* Laying the stones for Velocity forwarding

* Add vote forwarding to Velocity port

* It compiles, enables, and accepts a vote! (Still unfinished.)

* Vote forwarding works.

* Fix Sponge plugin message forwarding config

* Work around BungeeCord problems:

- JsonObject#keySet() is missing in Gson 2.8.0
- Bungee's "special" logging situation

* Remove Velocity repo

* Resolve issues that came up during review

* Handle a null error consumer

* Make the setup documentation proxy-agnostic

* Fix grammar.

* Add ParallelBlock repo
2018-09-02 19:00:57 -04:00
Joseph Hirschfeld c0ef61c819 Fix usage documentation regarding 1.13 channels 2018-09-02 14:11:44 -04:00
Joseph Hirschfeld 09bbfa8629 Add nvreload documentation 2018-08-23 00:37:49 -04:00
Joseph Hirschfeld aef038ef3a So gradle was getting mad at me, so i updated shadow. 2018-08-23 00:35:49 -04:00
Joseph Hirschfeld ac3827625d Add bukkit and bungee nvreload commands 2018-08-23 00:34:24 -04:00
Joseph Hirschfeld acc092f955 Fix a race condition in APMFS
Fixes the condition where there are never any failures passed back to be
re-added to the cache since failures is returned far before
dumpVotesToServer populates it.

This one hurt to find, but at least we got it!
2018-08-03 11:18:48 -04:00
Joseph Hirschfeld b494e1dc9d Fix eviction logging 2018-08-02 12:04:02 -04:00
Joseph Hirschfeld 27a368bd08 This is a snapshot! (Sorry build servers) 2018-08-02 12:01:48 -04:00
Joseph Hirschfeld 3c5b533a44 ignore gradletasknamecache
idk why its there, but it is, and i don't like it.
2018-08-02 12:00:37 -04:00
Joseph Hirschfeld f569c725d0 Signify that we are 1.13 compliant.
We should be at least. We just kick off events, no major
incompatibility.
2018-08-02 12:00:20 -04:00
Joseph Hirschfeld a09bc5a131 change default plugin messaging channel. Fixes #90
from NuVotifier to NuVotifier:votes. Gotta love stupid api changes that
have no reason to happen.
2018-08-02 12:00:01 -04:00
Joseph Hirschfeld c4e4f6e416 Close serverChannel and serverGroup in sync. Fixes #80
Patch originally authored by Time6628 on NytroCO/NuVotifier commit
5ee279bef03da9510952008e5ee5c8ada546cdb9 with modifications. In
addition, changes were applied to all three listeners.

Root issue caused by the fact that sometimes cleanups took longer than
Bukkit / anything else decided the jar holding the code should be open.
2018-08-02 11:47:17 -04:00
Parker Turchan 6ed181e835 Make the plugin actually work on Sponge API7 and cleanup a few other things (#85)
* fix injection problems

* more attempts at fixing weird NPEs

* fix injection issues and a bit of code cleanup

* fix some problems with the config and change it back to YAML so old configs still work, may add a converter to convert old YAML configs to HOCON in the future.

* clean up some other little things

* move constructor of ConfigLoader to loadConfig
2018-08-02 11:31:52 -04:00
99 changed files with 704 additions and 3846 deletions

No files matched your search

-16
View File
@@ -1,16 +0,0 @@
pipeline:
build:
image: cogniteev/oracle-java
commands: ./gradlew build test --no-daemon
publish:
image: cogniteev/oracle-java
commands:
- ./gradlew publishversionedPublicationToParallelBlockRepository
- ./gradlew publishLatestToParallelBlockRaw
environment:
- PUBLISH=yes
- LATEST_BUILD=yes
secrets: [SNAPSHOT_REPO, RELEASE_REPO, REPO_USERNAME, REPO_PASSWORD, RAW_UPLOAD_PATH]
when:
branch: master
event: push
+1
View File
@@ -0,0 +1 @@
custom: https://www.paypal.me/ibj
+1
View File
@@ -46,3 +46,4 @@ debug.txt
# gradle stuff
.gradle/
.gradletasknamecache
Vendored
+37
View File
@@ -0,0 +1,37 @@
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'
}
}
}
}
+14 -24
View File
@@ -1,35 +1,25 @@
# 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 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.
## Why fork Votifier?
NuVotifier supports many different platforms using the same universal JAR:
Votifier has lagged in activity, has severe security flaws in its protocol, and
is vulnerable to denial of service attacks.
+ Bukkit / Spigot / Paper >=1.8
+ Sponge 7
+ BungeeCord / Waterfall
+ Velocity
## Is NuVotifier a drop-in replacement for Votifier?
NuVotifier also adds forwarding and listener test commands not present in the
original version.
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.
[Setup Guide](https://github.com/NuVotifier/NuVotifier/wiki/Setup-Guide)
# Documentation / Configuration Guide
[Troubleshooting Guide](https://github.com/NuVotifier/NuVotifier/wiki/Troubleshooting-Guide)
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)
[Developer Information](https://github.com/NuVotifier/NuVotifier/wiki/Developer-Documentation)
# 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).
+18 -8
View File
@@ -2,33 +2,43 @@
import org.apache.tools.ant.filters.ReplaceTokens
allprojects {
group 'com.vexsoftware'
version '2.3.7'
group 'io.ibj.nuvotifier'
version '3.0.0-SNAPSHOT'
}
subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven {
url 'https://hub.spigotmc.org/nexus/content/groups/public/'
url "https://hub.spigotmc.org/nexus/content/repositories/snapshots"
}
maven {
url "https://oss.sonatype.org/content/groups/public/"
url "https://oss.sonatype.org/content/repositories/snapshots"
}
maven {
url "http://repo.spongepowered.org/maven"
url "https://repo.spongepowered.org/maven"
}
maven {
url "https://repo.velocitypowered.com/snapshots/"
}
}
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])
}
}
if (System.env.PUBLISH) {
apply from: 'publish.gradle'
test {
useJUnitPlatform()
}
}
+4
View File
@@ -0,0 +1,4 @@
dependencies {
api group: "org.spigotmc", name: "spigot-api", version: "1.8.8-R0.1-SNAPSHOT"
implementation project(":model")
}
@@ -1,5 +1,6 @@
package com.vexsoftware.votifier.model;
import io.ibj.nuvotifier.model.NuVote;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@@ -19,14 +20,14 @@ public class VotifierEvent extends Event {
/**
* Encapsulated vote record.
*/
private Vote vote;
private NuVote vote;
/**
* Constructs a vote event that encapsulated the given vote record.
*
* @param vote vote record
*/
public VotifierEvent(final Vote vote) {
public VotifierEvent(final NuVote vote) {
this.vote = vote;
}
-5
View File
@@ -1,5 +0,0 @@
dependencies {
compileOnly group: "org.spigotmc", name: "spigot-api", version: "1.8.8-R0.1-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -1,388 +0,0 @@
/*
* Copyright (C) 2012 Vex Software LLC
* This file is part of Votifier.
*
* Votifier is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Votifier is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Votifier. If not, see <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.forwarding.ForwardedVoteListener;
import com.vexsoftware.votifier.forwarding.ForwardingVoteSink;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.model.VotifierEvent;
import com.vexsoftware.votifier.net.VotifierSession;
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.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.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
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 channel.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
/**
* 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;
@Override
public void onEnable() {
NuVotifierBukkit.instance = this;
// Set the plugin version.
version = getDescription().getVersion();
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
// Handle configuration.
File config = new File(getDataFolder() + "/config.yml");
YamlConfiguration cfg;
File rsaDirectory = new File(getDataFolder() + "/rsa");
/*
* 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.write(cfgStr, config, StandardCharsets.UTF_8);
/*
* 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);
gracefulExit();
return;
}
}
// 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);
gracefulExit();
return;
}
debug = cfg.getBoolean("debug", false);
// 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);
gracefulExit();
return;
}
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("DEBUG 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("------------------------------------------------------------------------------");
}
serverGroup = new NioEventLoopGroup(1);
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(serverGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(VotifierPlugin.KEY).set(NuVotifierBukkit.this);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, !disablev1));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(NuVotifierBukkit.this));
}
})
.bind(host, port)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
serverChannel = future.channel();
getLogger().info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
} else {
SocketAddress socketAddress = future.channel().localAddress();
if (socketAddress == null) {
socketAddress = new InetSocketAddress(host, port);
}
getLogger().log(Level.SEVERE, "Votifier was not able to bind to " + socketAddress, future.cause());
}
}
});
} 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.");
}
}
}
@Override
public void onDisable() {
// Shut down the network handlers.
if (serverGroup != null) {
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
}
if (forwardingMethod != null) {
forwardingMethod.halt();
}
getLogger().info("Votifier disabled.");
}
private void gracefulExit() {
getLogger().log(Level.SEVERE, "Votifier did not initialize properly!");
}
/**
* Gets the instance.
*
* @return The instance
*/
public static NuVotifierBukkit getInstance() {
return instance;
}
/**
* Gets the version.
*
* @return The version
*/
public String getVersion() {
return version;
}
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) throws Exception {
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, new Runnable() {
@Override
public void run() {
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, new Runnable() {
@Override
public void run() {
Bukkit.getPluginManager().callEvent(new VotifierEvent(v));
}
});
}
}
@@ -1,49 +0,0 @@
package com.vexsoftware.votifier.forwarding;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.NuVotifierBukkit;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.util.GsonInst;
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.nio.charset.StandardCharsets;
import java.util.logging.Level;
/**
* Created by Joe Hirschfeld on 10/20/2015.
*/
public class BukkitPluginMessagingForwardingSink implements ForwardingVoteSink, PluginMessageListener {
public BukkitPluginMessagingForwardingSink(Plugin p, String channel, ForwardedVoteListener listener) {
Validate.notNull(channel, "Channel cannot be null.");
this.channel = channel;
Bukkit.getMessenger().registerIncomingPluginChannel(p, channel, this);
this.listener = listener;
this.p = p;
}
private final Plugin p;
private final String channel;
private final ForwardedVoteListener listener;
@Override
public void halt() {
Bukkit.getMessenger().unregisterIncomingPluginChannel(p, channel, this);
}
@Override
public void onPluginMessageReceived(String s, Player player, byte[] bytes) {
try {
String message = new String(bytes, StandardCharsets.UTF_8);
JsonObject jsonObject = GsonInst.gson.fromJson(message, JsonObject.class);
Vote v = new Vote(jsonObject);
listener.onForward(v);
} catch (Exception e) {
NuVotifierBukkit.getInstance().getLogger().log(Level.SEVERE, "There was an unknown error when processing a forwarded vote.", e);
}
}
}
@@ -1,17 +0,0 @@
package com.vexsoftware.votifier.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);
}
@@ -1,13 +0,0 @@
package com.vexsoftware.votifier.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();
}
@@ -1,29 +0,0 @@
# 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
# Whether or not to print debug messages. In a production system, this should be set to false.
# This is useful when initially setting up NuVotifier to ensure votes are being delivered.
debug: false
# 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
-5
View File
@@ -1,5 +0,0 @@
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: [ParallelBlock, blakeman8192, Kramer, tuxed]
+4
View File
@@ -0,0 +1,4 @@
dependencies {
api group: "net.md-5", name: "bungeecord-api", version: "1.8-SNAPSHOT"
implementation project(":model")
}
@@ -1,6 +1,6 @@
package com.vexsoftware.votifier.bungee.events;
import com.vexsoftware.votifier.model.Vote;
import io.ibj.nuvotifier.model.NuVote;
import net.md_5.bungee.api.plugin.Event;
/**
@@ -11,13 +11,13 @@ public class VotifierEvent extends Event {
/**
* The vote.
**/
private final Vote vote;
private final NuVote vote;
public VotifierEvent(Vote vote) {
public VotifierEvent(NuVote vote) {
this.vote = vote;
}
public Vote getVote() {
public NuVote getVote() {
return vote;
}
}
-4
View File
@@ -1,4 +0,0 @@
dependencies {
compileOnly group: "net.md-5", name: "bungeecord-api", version: "1.8-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -1,396 +0,0 @@
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.VotifierPlugin;
import com.vexsoftware.votifier.bungee.events.VotifierEvent;
import com.vexsoftware.votifier.bungee.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.bungee.forwarding.OnlineForwardPluginMessagingForwardingSource;
import com.vexsoftware.votifier.bungee.forwarding.PluginMessagingForwardingSource;
import com.vexsoftware.votifier.bungee.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.bungee.forwarding.cache.MemoryVoteCache;
import com.vexsoftware.votifier.bungee.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.bungee.forwarding.proxy.ProxyForwardingVoteSource;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
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.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.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import net.md_5.bungee.api.ProxyServer;
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.SocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
/**
* The server channel.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
/**
* 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;
@Override
public void onEnable() {
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.write(cfgStr, config, StandardCharsets.UTF_8);
/*
* 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);
debug = configuration.getBoolean("debug", false);
if (debug)
getLogger().info("DEBUG 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(new Runnable() {
@Override
public void run() {
serverGroup = new NioEventLoopGroup(2);
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(serverGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(VotifierPlugin.KEY).set(NuVotifier.this);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, !disablev1));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(NuVotifier.this));
}
})
.bind(host, port)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
serverChannel = future.channel();
getLogger().info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
} else {
SocketAddress socketAddress = future.channel().localAddress();
if (socketAddress == null) {
socketAddress = new InetSocketAddress(host, port);
}
getLogger().log(Level.SEVERE, "Votifier was not able to bind to " + socketAddress, future.cause());
}
}
});
}
}));
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());
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);
}
}
if (!fwdCfg.getBoolean("pluginMessaging.onlySendToJoinedServer")) {
List<String> ignoredServers = fwdCfg.getStringList("pluginMessaging.excludedServers");
try {
forwardingMethod = new PluginMessagingForwardingSource(channel, ignoredServers, this, voteCache);
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, voteCache, fallbackServer);
} 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, serverGroup, 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 onDisable() {
// Shut down the network handlers.
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
if (forwardingMethod != null) {
forwardingMethod.halt();
}
getLogger().info("Votifier disabled.");
}
@Override
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
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, new Runnable() {
@Override
public void run() {
getProxy().getPluginManager().callEvent(new VotifierEvent(vote));
}
});
if (forwardingMethod != null) {
getProxy().getScheduler().runAsync(this, new Runnable() {
@Override
public void run() {
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 String getVersion() {
return getDescription().getVersion();
}
public boolean isDebug() {
return debug;
}
}
@@ -1,141 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding;
import com.vexsoftware.votifier.bungee.NuVotifier;
import com.vexsoftware.votifier.bungee.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.bungee.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.model.Vote;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.event.EventHandler;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public abstract class AbstractPluginMessagingForwardingSource implements ForwardingVoteSource {
public AbstractPluginMessagingForwardingSource(String channel, List<String> ignoredServers, NuVotifier nuVotifier, VoteCache cache) {
ProxyServer.getInstance().registerChannel(channel);
this.channel = channel;
this.nuVotifier = nuVotifier;
this.cache = cache;
this.ignoredServers = ignoredServers;
}
protected AbstractPluginMessagingForwardingSource(String channel, NuVotifier nuVotifier, VoteCache voteCache) {
this(channel, null, nuVotifier, voteCache);
}
protected final NuVotifier nuVotifier;
protected final String channel;
protected final VoteCache cache;
protected final List<String> ignoredServers;
@Override
public void forward(Vote v) {
byte[] rawData = v.serialize().toString().getBytes(StandardCharsets.UTF_8);
for (ServerInfo s : ProxyServer.getInstance().getServers().values()) {
if (ignoredServers != null && ignoredServers.contains(s.getName())) continue;
if (!forwardSpecific(s, rawData)) attemptToAddToCache(v, s.getName());
}
}
protected boolean forwardSpecific(ServerInfo connection, Vote vote) {
byte[] rawData = vote.serialize().toString().getBytes(StandardCharsets.UTF_8);
return connection.sendData(channel, rawData, false);
}
private boolean forwardSpecific(ServerInfo connection, byte[] data) {
return connection.sendData(channel, data, false);
}
@Override
public void halt() {
ProxyServer.getInstance().unregisterChannel(channel);
if (cache != null && cache instanceof FileVoteCache) {
try {
((FileVoteCache) cache).halt();
} catch (IOException e) {
nuVotifier.getLogger().log(Level.SEVERE, "Unable to save cached votes, votes will be lost.", e);
}
}
}
protected void handleServerConnected(final ServerConnectedEvent e) {
if (cache == null) return;
final String serverName = e.getServer().getInfo().getName();
final Collection<Vote> cachedVotes = cache.evict(serverName);
final Collection<Vote> failedVotes = dumpVotesToServer(cachedVotes, e.getServer().getInfo(), "server '" + serverName + "'");
for (Vote v : failedVotes)
cache.addToCache(v, serverName);
}
protected void attemptToAddToCache(Vote v, String server) {
if (cache != null) {
cache.addToCache(v, server);
if (nuVotifier.isDebug())
nuVotifier.getLogger().info("Added to forwarding cache: " + v + " -> " + server);
} else if (nuVotifier.isDebug())
nuVotifier.getLogger().severe("Could not immediately send vote to backend, vote lost! " + v + " -> " + server);
}
protected void attempToAddToPlayerCache(Vote v, String player) {
if (cache != null) {
cache.addToCachePlayer(v, player);
if (nuVotifier.isDebug())
nuVotifier.getLogger().info("Added to forwarding cache: " + v + " -> (player) " + player);
} else if (nuVotifier.isDebug())
nuVotifier.getLogger().severe("Could not immediately send vote to backend, vote lost! " + v + " -> (player) " + player);
}
// returns a collection of failed votes
private Collection<Vote> dumpVotesToServer(Collection<Vote> cachedVotes, ServerInfo target, String identifier) {
List<Vote> failures = new ArrayList<>();
if (!cachedVotes.isEmpty()) {
nuVotifier.getProxy().getScheduler().schedule(nuVotifier, new Runnable() {
@Override
public void run() {
int evicted = 0;
int unsuccessfulEvictions = 0;
for (Vote v : cachedVotes) {
if (forwardSpecific(target, v)) {
evicted++;
} else {
// Re-add to cache to send later.
failures.add(v);
unsuccessfulEvictions++;
}
}
if (nuVotifier.isDebug()) {
nuVotifier.getLogger().info("Successfully evicted " + evicted + " votes to " + identifier + ".");
if (unsuccessfulEvictions > 0) {
nuVotifier.getLogger().info("Held " + unsuccessfulEvictions + " votes for " + identifier + ".");
}
}
}
}, 1, TimeUnit.SECONDS);
}
return failures;
}
protected void handlePlayerSwitch(ServerConnectedEvent e) {
if (cache == null) return;
if (ignoredServers != null && ignoredServers.contains(e.getServer().getInfo().getName())) return;
String playerName = e.getPlayer().getName();
final Collection<Vote> cachedVotes = cache.evictPlayer(playerName);
final Collection<Vote> failedVotes = dumpVotesToServer(cachedVotes, e.getServer().getInfo(), "player '" + playerName);
for (Vote v : failedVotes)
cache.addToCachePlayer(v, playerName);
}
}
@@ -1,22 +0,0 @@
package com.vexsoftware.votifier.bungee.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();
}
@@ -1,58 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding;
import com.vexsoftware.votifier.bungee.NuVotifier;
import com.vexsoftware.votifier.bungee.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.model.Vote;
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, VoteCache cache, String fallbackServer) {
super(channel, nuVotifier, cache);
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 || !forwardSpecific(p.getServer().getInfo(), v)) {
ServerInfo serverInfo = ProxyServer.getInstance().getServers().get(fallbackServer);
// nowhere to fall back to, yet still not online. lets save this vote yet!
if (serverInfo == null)
attempToAddToPlayerCache(v, v.getUsername());
else if (!forwardSpecific(serverInfo, v))
attemptToAddToCache(v, fallbackServer);
}
}
// The below is to allow us to share code since the event handlers from the parent class don't get
// picked up.
@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.
handleServerConnected(e);
}
@EventHandler
public void onPlayerSwitch(ServerConnectedEvent e) {
handlePlayerSwitch(e);
}
}
@@ -1,34 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding;
import com.vexsoftware.votifier.bungee.NuVotifier;
import com.vexsoftware.votifier.bungee.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;
import java.util.List;
public class PluginMessagingForwardingSource extends AbstractPluginMessagingForwardingSource implements Listener {
public PluginMessagingForwardingSource(String channel, List<String> ignoredServers, NuVotifier nuVotifier, VoteCache cache) {
super(channel, ignoredServers, nuVotifier, cache);
ProxyServer.getInstance().getPluginManager().registerListener(nuVotifier, this);
}
protected PluginMessagingForwardingSource(String channel, NuVotifier nuVotifier, VoteCache voteCache) {
super(channel, nuVotifier, voteCache);
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.
handleServerConnected(e);
}
}
@@ -1,167 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.cache;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.util.GsonInst;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class FileVoteCache extends MemoryVoteCache {
private final Logger l;
private final File cacheFile;
private final int voteTTL;
private final ScheduledTask saveTask;
public FileVoteCache(int initialMemorySize, final Plugin plugin, File cacheFile, int voteTTL) throws IOException {
super(initialMemorySize);
this.cacheFile = cacheFile;
this.voteTTL = voteTTL;
this.l = plugin.getLogger();
load();
saveTask = ProxyServer.getInstance().getScheduler().schedule(plugin, new Runnable() {
@Override
public void run() {
try {
save();
} catch (IOException e) {
l.log(Level.SEVERE, "Unable to save cached votes, votes will be lost if you restart.", e);
}
}
}, 3, 3, TimeUnit.MINUTES);
}
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 : players.keySet()) {
playerVoteCache.put(player, readVotes(players.getAsJsonArray(player)));
}
for (String server : servers.keySet()) {
voteCache.put(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.log(Level.SEVERE, "Backup movement failed! Will not save.");
return;
}
l.log(Level.WARNING, "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.log(Level.WARNING, "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));
votesObject.add("servers", serializeMap(voteCache));
} 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.log(Level.WARNING, "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;
}
private boolean hasTimedOut(Vote v) {
if (voteTTL == -1) return false;
// scale voteTTL to milliseconds
return v.getLocalTimestamp() + voteTTL * 24 * 60 * 60 * 1000 < System.currentTimeMillis();
}
public void halt() throws IOException {
saveTask.cancel();
save();
}
}
@@ -1,90 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.cache;
import com.google.common.collect.ImmutableList;
import com.vexsoftware.votifier.model.Vote;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Joe Hirschfeld on 10/20/2015.
*/
public class MemoryVoteCache implements VoteCache {
public MemoryVoteCache(int initialSize) {
voteCache = new HashMap<>(initialSize);
this.playerVoteCache = new HashMap<>();
}
protected final Map<String, Collection<Vote>> voteCache;
protected final Map<String, Collection<Vote>> playerVoteCache;
protected final ReentrantLock cacheLock = new ReentrantLock();
@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 {
Collection<Vote> voteCollection = voteCache.get(server);
if (voteCollection == null) {
voteCollection = new ArrayList<>();
voteCache.put(server, voteCollection);
}
voteCollection.add(v);
} finally {
cacheLock.unlock();
}
}
@Override
public void addToCachePlayer(Vote v, String player) {
if (player == null) throw new NullPointerException();
cacheLock.lock();
try {
Collection<Vote> voteCollection = playerVoteCache.get(player);
if (voteCollection == null) {
voteCollection = new ArrayList<>();
playerVoteCache.put(player, voteCollection);
}
voteCollection.add(v);
} finally {
cacheLock.unlock();
}
}
@Override
public Collection<Vote> evictPlayer(String player) {
if (player == null) throw new NullPointerException();
cacheLock.lock();
try {
Collection<Vote> fromCollection = playerVoteCache.remove(player);
return fromCollection != null ? fromCollection : ImmutableList.of();
} finally {
cacheLock.unlock();
}
}
@Override
public Collection<Vote> evict(String server) {
if (server == null) throw new NullPointerException();
cacheLock.lock();
try {
Collection<Vote> fromCollection = voteCache.remove(server);
return fromCollection != null ? fromCollection : ImmutableList.of();
} finally {
cacheLock.unlock();
}
}
}
@@ -1,56 +0,0 @@
package com.vexsoftware.votifier.bungee.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;
}
}
@@ -1,140 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy;
import com.vexsoftware.votifier.bungee.NuVotifier;
import com.vexsoftware.votifier.bungee.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.bungee.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.bungee.forwarding.proxy.client.VotifierProtocol2Encoder;
import com.vexsoftware.votifier.bungee.forwarding.proxy.client.VotifierProtocol2HandshakeHandler;
import com.vexsoftware.votifier.bungee.forwarding.proxy.client.VotifierResponseHandler;
import com.vexsoftware.votifier.model.Vote;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.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.logging.Level;
public class ProxyForwardingVoteSource implements ForwardingVoteSource {
private static final int MAX_RETRIES = 5;
private final NuVotifier plugin;
private final NioEventLoopGroup eventLoopGroup;
private final List<BackendServer> backendServers;
private final VoteCache voteCache;
private static final StringDecoder STRING_DECODER = new StringDecoder(StandardCharsets.US_ASCII);
public ProxyForwardingVoteSource(NuVotifier plugin, NioEventLoopGroup eventLoopGroup, List<BackendServer> backendServers, VoteCache voteCache) {
this.plugin = plugin;
this.eventLoopGroup = eventLoopGroup;
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) {
new Bootstrap()
.channel(NioSocketChannel.class)
.group(eventLoopGroup)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new DelimiterBasedFrameDecoder(256, true, Delimiters.lineDelimiter()));
channel.pipeline().addLast(new ReadTimeoutHandler(8, 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.getLogger().info("Successfully forwarded vote " + v + " to " + server.address + ".");
}
}
@Override
public void onFailure(Throwable error) {
handleFailure(server, v, error, tries);
}
}, plugin));
}
})
.connect(server.address)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
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.getLogger().log(Level.SEVERE, msg, cause);
} else {
plugin.getLogger().log(Level.SEVERE, msg);
}
if (willRetry) {
plugin.getProxy().getScheduler().schedule(plugin, new Runnable() {
@Override
public void run() {
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;
}
}
}
@@ -1,29 +0,0 @@
package com.vexsoftware.votifier.bungee.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 +
'}';
}
}
@@ -1,46 +0,0 @@
package com.vexsoftware.votifier.bungee.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();
}
}
@@ -1,42 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
import com.vexsoftware.votifier.bungee.NuVotifier;
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 NuVotifier nuVotifier;
public VotifierProtocol2HandshakeHandler(Vote toSend, VotifierResponseHandler responseHandler, NuVotifier nuVotifier) {
this.toSend = toSend;
this.responseHandler = responseHandler;
this.nuVotifier = nuVotifier;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
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.getLogger().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) throws Exception {
responseHandler.onFailure(cause);
ctx.close();
}
}
@@ -1,33 +0,0 @@
package com.vexsoftware.votifier.bungee.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) throws Exception {
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) throws Exception {
responseHandler.onFailure(cause);
ctx.close();
}
}
@@ -1,7 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
public interface VotifierResponseHandler {
void onSuccess();
void onFailure(Throwable error);
}
-4
View File
@@ -1,4 +0,0 @@
name: NuVotifier
main: com.vexsoftware.votifier.bungee.NuVotifier
version: @app.version@
author: ParallelBlock, blakeman8192, Kramer, tuxed
@@ -1,67 +0,0 @@
# 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
# Whether or not to print debug messages. In a production system, this should be set to false.
# This is useful when initially setting up NuVotifier to ensure votes are being delivered.
debug: 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%
# 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
#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, this option is ignored.
#excludedServers:
#- server1
#- server2
# 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.
# This method is NOT recommended for production systems!
# - 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'
# Options for file caching.
file:
name: cached-votes.json
# days before a vote is considered 'dead' - removed from cache with a console warning
voteTTL: 10
# 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
@@ -1,7 +0,0 @@
dependencies {
compile group: "io.netty", name: "netty-handler", version: "4.1.16.Final"
compile 'com.google.code.gson:gson:2.8.2'
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
}
@@ -1,20 +0,0 @@
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");
}
}
@@ -1,17 +0,0 @@
package com.vexsoftware.votifier;
import io.netty.util.AttributeKey;
import java.security.Key;
import java.security.KeyPair;
import java.util.Map;
public interface VotifierPlugin {
AttributeKey<VotifierPlugin> KEY = AttributeKey.valueOf("votifier_plugin");
Map<String, Key> getTokens();
KeyPair getProtocolV1Key();
String getVersion();
}
@@ -1,213 +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;
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;
}
}
@@ -1,11 +0,0 @@
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,89 +0,0 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.VoteInboundHandler;
import com.vexsoftware.votifier.net.protocol.VotifierGreetingHandler;
import com.vexsoftware.votifier.net.protocol.VotifierProtocolDifferentiator;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.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.security.Key;
import java.security.KeyPair;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class StandaloneVotifierPlugin implements VotifierPlugin, VoteHandler {
private final Map<String, Key> tokens;
private final VoteReceiver receiver;
private final KeyPair v1Key;
private final InetSocketAddress bind;
private NioEventLoopGroup group;
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;
}
public Throwable start() throws Throwable {
group = new NioEventLoopGroup(2);
ChannelFuture future = new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(group)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(VotifierPlugin.KEY).set(StandaloneVotifierPlugin.this);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, v1Key != null));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(StandaloneVotifierPlugin.this));
}
})
.bind(bind)
.syncUninterruptibly();
if (!future.isSuccess()) {
return future.cause();
} else {
return null;
}
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return v1Key;
}
@Override
public String getVersion() {
return "2.3.5";
}
@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);
}
}
@@ -1,8 +0,0 @@
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);
}
@@ -1,57 +0,0 @@
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);
}
}
@@ -1,87 +0,0 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.util.KeyCreator;
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 String getVersion() {
return "2.3";
}
public void specificKeysOnly() {
keyMap.clear();
keyMap.put("Test", KeyCreator.createKeyFrom("test"));
}
public void restoreDefault() {
keyMap.clear();
keyMap.put("default", KeyCreator.createKeyFrom("test"));
}
}
@@ -1,31 +0,0 @@
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);
}
}
@@ -1,89 +0,0 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.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();
}
}
}
@@ -1,148 +0,0 @@
package com.vexsoftware.votifier.net.protocol;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.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() 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();
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);
}
}
@@ -1,95 +0,0 @@
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();
}
}
-1
View File
@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArrOsFuGPoHDZEFRHXriZRqrUXXjUOhVabC0Xu+WPtVbQrvNngN0igo0ZJIZELflhsFNBUJAnREo+wColfDg1rk2juEUYDt+gRzrN3WiqgzZolF3E4Dg4LrXh6cPuu4AeOZW9CtjXt2JwPBhlWLZb2RFhA6FziFLeMBa42/5VLFCQAmiuuU3OCiBskR3Jz/E5zqc7V/j247OMm+WRJ/k8OFTEFB+zFINOZY1ud3ltwkzQIPi1luGNW8Yt8+CEg/6hDiVlpFwgsazt4uKI/UfU5oP0kAKjQUfFGvNaBtJ0RlsBeJ7VDpOfIG6dDb8pe4IaA9aJLVcXWEn30pHsyYl2NwIDAQAB
-1
View File
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAneaLKDb6CzOdqTfFNeUJEV6RlcZNFmqDS1oDduKZ1t9BSADt26fKg9hVyc19ob8ONyyMLnVRG8Jnc3srVVgzbooC6hLCSbFwh4fTkSoxlR70tuNK+onX8Sdd/Sc9nd6EmRvGnB3R4LPx/zPfDvJc/WW9ih7Zvq8aYsLFdknYTLXzAyZkceKdCZJYCfE1dWQKaMqWxrUSlxzoq1UrOs34GmGnoJVDSEwILGWTWZPo/T6ee9hccmK8sZFGllZ9PUQA9O171iiXN3+zQRJQNi6bmV02CYdwV2iPzdObXYD3o+aPfT12gNlDIXaqO+/M6EKHlszr2zTFKmUTqhluVg8IXQIDAQAB
-80
View File
@@ -1,80 +0,0 @@
This document outlines various developer resources concerning NuVotifier.
# Contributing
NuVotifier is an open source GNU GPLv3 licensed project. We accept contributions
through pull requests, and will make sure to credit you for your gracious
contribution.
# Building NuVotifier
NuVotifier can be built by running the following: `./gradlew build` or
`./gradlew.bat build` on Windows. The resultant universal jar is built and
written to `universal/build/libs/nuvotifier-{version}.jar`.
The build directories can be cleaned instead using the `./gradlew clean` (and
`./gradlew.bat build`) command.
# NuVotifier as a plugin dependency
ParallelBlock now maintains an independent Maven repository for free use by the
community. Builds are automatically published by a CI operated by ParallelBlock.
## Maven
You can have your project depend on NuVotifier as a dependency through the
following code snippets:
```
<project>
...
<repositories>
<repository>
<id>parallelblock-public</id>
<name>ParallelBlock Public Repository</name>
<url>https://repo.parallelblock.com/repository/maven-public/</url>
</repository>
...
</repositories>
...
<dependencies>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-universal</artifactId>
<version>...</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
...
</project>
```
## Gradle
You can include NuVotifier into your gradle project using the following lines:
```
...
repositories {
maven {
url 'https://repo.parallelblock.com/repository/maven-public/'
}
...
}
...
dependencies {
compileOnly "com.vexsoftware:nuvotifier-universal:verison"
...
}
...
```
# Implementing NuVotifier v2 protocol in a server list
NuVotifier provides production ready reference implementations for interacting
with NuVotifier servers. These libraries are available below:
- [NodeJS](https://github.com/NuVotifier/votifier2-js)
- [PHP](https://github.com/NuVotifier/votifier2-php)
- [Golang](https://github.com/minecrafter/go-votifier)
-16
View File
@@ -1,16 +0,0 @@
# Documentation
This directory holds documentation for NuVotifier.
# Usage Guide
The usage/configuration guide may be found [here](usage.md).
# Developer Guide
The developer guide for NuVotifier may be found [here](developer.md).
# Technical QA
Some assorted technical questions not important to most people are found
[here](technical_qa.md).
-78
View File
@@ -1,78 +0,0 @@
This document outlines some of the technical reasons for a new NuVotifier.
# Why a new protocol?
The old Votifier protocol has three major flaws, which we feel are corrected in the new protocol.
* The data is encrypted (with RSA) but not hashed to verify its integrity. The new protocol utilizes HMAC with SHA256 (but no encryption, given that the data sent is eventually revealed in some form or another).
* The canonical Votifier protocol is vulnerable to replay attacks.
* The canonical Votifier is one key for all clients, which is weak. While NuVotifier still supports a default key (for the less paranoid), this ability can be disabled and force per-server keys.
# Is NuVotifier a drop-in replacement for Votifier?
Yes. NuVotifier is compatible with Votifier server lists and Votifier listeners, with some differences:
* The `Vote` object's setters are deprecated.
* Support for standalone vote listeners (`.class` files) has been removed, as handling `VotifierEvent` is more flexible.
# Protocol Documentation
This documentation is for server lists that wish to add NuVotifier support.
There are two versions of the NuVotifier protocol.
## Handshake
A connection is made to the NuVotifier server by the server list, and immediately NuVotifier will send its version in the following packet:
"VOTIFIER <version> <challenge>"
The challenge will not be present in version 1.x servers.
## Protocol v2
NuVotifier expects a message composed of `0x733A` (16-bit big-endian integer) and the length of the following JSON message (as a 16-bit big-endian integer) sent plus the message.
The message is JSON-encoded containing the following data:
* `payload` contains the fully encoded vote. It is a JSON message containing the `serviceName` (string), `username` (string), `address` (string, but must be a valid IPv4 or IPv6 address), `timestamp` (a long with the time this vote was taken in _milliseconds_), and `challenge` (from the handshake). It is optional, but recommended to supply a `uuid` field with the player's UUID (with dashes, e.g. `652a2bc4-e8cd-405d-b7b6-98156ee2dc09`).
* `signature` is a HMAC-SHA256 digest of the payload with a owner-provided key.
A response is given after the vote has been received and parsed, but not yet processed.
For your convenience, the NuVotifier team has provided client libraries in [JavaScript](https://github.com/NuVotifier/votifier2-js), [PHP](https://github.com/NuVotifier/votifier2-php) and [Go](https://github.com/minecrafter/go-votifier). If you are stuck, these libraries may help you.
## Protocol v1 (deprecated)
For better compatibility, NuVotifier supports the old Votifier protocol as well.
NuVotifier expects a 256 byte RSA encrypted block (the public key should be obtained by the NuVotifier user), with the following format:
<table>
<tr>
<th>Type</th>
<th>Value</th>
</tr>
<tr>
<td>string</td>
<td>VOTE</td>
</tr>
<tr>
<td>string</td>
<td>serviceName</td>
</tr>
<tr>
<td>string</td>
<td>username</td>
</tr>
<tr>
<td>string</td>
<td>address</td>
</tr>
<tr>
<td>string</td>
<td>timeStamp</td>
</tr>
</table>
The first string of value "VOTE" is an opcode check to ensure that RSA was encoded and decoded properly, if this value is wrong then NuVotifier assumes that there was a problem with encryption and drops the connection. `serviceName` is the name of the top list service, `username` is the username (entered by the voter) of the person who voted, `address` is the IP address of the voter, and `timeStamp` is the time stamp of the vote. Each string is delimited by the newline character `\n` (byte value 10). The `space` block is the empty space that is left over, **the block must be exactly 256 bytes** regardless of how much information it holds.
-231
View File
@@ -1,231 +0,0 @@
This document outlines how to configure NuVotifier for a variety of different
configurations. It should be used by server administrators looking to set up
NuVotifier on their network.
# Key vs Token
This document uses the terms `token` and `key`. These are two separate terms,
and it is important to understand the differences.
## Key
Keys in terms of NuVotifier refer to the old Votifier style RSA keys. They
are typically distributed as a pair of files - a public and private key. When
dealing with RSA keys, you can share your public key with server lists, but make
sure to keep your private key safe with you. You can find these keys
auto-generated under `plugins/(Nu)Votifier/rsa/`, where your public key is
named `public.key`, while your private key is labeled `private.key`. Make sure
not to separate the two, as they won't work together.
## Token
Tokens in terms of NuVotifier refer to a new short string of characters. This
string of characters is secret, and should never be exposed to anyone but you or
server lists which support NuVotifier voting (check with the server list
first). Tokens are most commonly used internally within a network for use when
using the proxy vote forwarding method.
# Single Server Configuration (Bukkit / Spigot / Sponge)
You will want to set up your server using these instructions when you only have
one Minecraft server (Bukkit / Sponge). These are the most trivial installation
steps.
## Installing the plugin on a server
To install NuVotifier, you should get a copy from one of the following
locations:
+ [Github](https://github.com/parallelblock/NuVotifier/releases) - Official
release location
+ [SpigotMC](https://www.spigotmc.org/resources/nuvotifier.13449/) - SpigotMC
release mirror
+ [repo.parallelblock.com](https://repo.parallelblock.com/repository/public-raw/nuvotifier/nuvotifier.jar) - ParallelBlock CI 'hot off the presses' releases
The NuVotifier jar should then be placed within the `plugins/` folder of your
Minecraft server. When you then start your server, NuVotifier will generate
the necessary configuration and initial keys for you.
## Configuring the plugin
When running in a single server configuration, NuVotifier's default
configuration works well. The only value which you may need to change is the
`port` configuration value - this specifies the port on which NuVotifier
listens to new votes. This port cannot be any port you are already using. For
example, if Minecraft is running on the default port (25565), you cannot set
`port` also to 25565. Make sure port is a number from 1-65535.
You can then add your server to server lists using the port you configured while
also supplying your public key. At this point, you should be able to send a test
vote to your server!
# Multi-server / Network configuration
You will want to use these instructions when you have more than one server and
are using a Minecraft proxy (Bungeecord / Lilypad)
## Single Bungeecord Networks
When running a single-bungeecord network, NuVotifier is able to operate as both
a Bungeecord vote receiver (emitting events for Bungeecord based vote listeners)
as well as a vote forwarder. NuVotifier's vote forwarding logic is also advanced
enough to be used in a wide variety of ways.
### Installing the plugin on the servers
NuVotifier should be installed on each of the servers you wish to process votes
on, as well as the Bungeecord server. The Bungeecord server will act as the
public facing Votifier instance, where as the Minecraft servers will instead
receive instructions from the Bungeecord Nuvotifier for forwarding votes.
Like, the single server installation, you can download NuVotifier at any one of
the following locations:
+ [Github](https://github.com/parallelblock/NuVotifier/releases) - Official
release location
+ [SpigotMC](https://www.spigotmc.org/resources/nuvotifier.13449/) - SpigotMC
release mirror
+ [repo.parallelblock.com](https://repo.parallelblock.com/repository/public-raw/nuvotifier/nuvotifier.jar) - ParallelBlock CI 'hot off the presses' releases
The plugin should then be placed in the `plugins/` directory of the Bungeecord
server as well as each of the Minecraft servers.
### Vote Forwarding
The following sections describe how vote forwarding works. Vote forwarding is an
advanced configurable operation, and requires a lot of configuration to get
correct.
### Plugin messaging based forwarding
This section describes how to set up plugin messaging forwarding. This will only
work correctly if you have one Bungeecord server, however may also work with
some exceptions in multi Bungeecord networks.
First off, we should configure each of the Minecraft servers. Open each of the
servers' NuVotifier configurations. Each one should have `method` under the
`forwarding` configuration section set to `pluginMessaging`. Under the
`pluginMessaging` configuration section, `channel` is set to `NuVotifier`. This
is a safe default (and is protected from injection by a hacked Minecraft
client), but feel free to change it. If you choose to change it, make sure you
change it everywhere all around your network!
Since we are receiving forwarded votes through plugin messaging and not through
network ports, we should set `port` on each of the Minecraft servers to -1. This
will disable NuVotifier's port, while still allowing it to receive forwarded
plugin messaging votes. This will only work when using pluginMessaging!
Since we have all of that configured, we can now move to the Bungeecord
NuVotifier configuration. It is once again located under `plugins/NuVotifier/`
directory (`config.yml`). Like the single server, `port` should be set to an
unbound port. The default `8192` works well, however you may have to change it
depending on your hosting provider.
Since we are using pluginMessaging based forwarding, lets set `method` under
`forwarding` to `pluginMessaging`. Make sure that `channel` is the same channel
that you configured your servers to listen to - if you didn't change this value,
then the default will work.
NuVotifier saves when it can't immediately forward in a user defined cache. In
almost all installations this should be `file`, however, some installations may
instead want `none` or `memory`. If you don't know, the `cache` should be set to
it's default, `file`.
By default, NuVotifier will forward the vote to all of the connected Minecraft
servers (all servers you can get to when typing /server). You can manually
exclude some of these servers by first uncommenting `excludedServers`, then
adding the server's name to the `excludedServers` list. After adding, NuVotifier
will not send any votes to this server. If you only send the vote to the server
the player joined, these servers are ignored.
NuVotifier can also selectively send the vote to only the server which the
player has joined. This option can be turned on by setting
`onlySendToJoinedServer` to true. When this option is on, the vote will attempt
to go to the server which the player is currently on. If the player is currently
not on a server (offline), then NuVotifier will then attempt to send the vote to
the `joinedServerFallback`. If the vote cannot be sent to the
`joinedServerFallback`, then the vote is saved in the cache until the vote can
be sent to the `joinedServerFallback` server - note this will not send it to the
next server the player joins! If you do not want a fallback server and would
rather the vote be applied when the player next joins, set
`joinedServerFallback` to `''`. This will cause NuVotifier to save the vote
until the player joins again.
By default, votes are saved for 10 days in the cache. This time can be increased
or decreased by changing the `voteTTL` field. This number can be as high or low
as you want. It is measured in days within the cache.
### Proxy based vote forwarding
In more complex network situations, it may be required to instead use the proxy
based vote forwarding. This configuration is less flexible and more difficult to
set up properly - most people should use plugin messaging based forwarding.
Proxy based forwarding does not support vote caching or advanced forwarding
configurations. If you cannot use plugin messaging forwarding, then the
following section will describe how to set up proxy based vote forwarding. If
you still require advanced forwarding features or more advanced behaviors, it
may be worthwhile to write your own Bungeecord plugin to perform the proper
forwarding logic (you will know if you need to do this).
In proxy based vote forwarding, one of your Bungeecord servers acts as the head
vote receiver. This server will be the one you configure the voting websites to
vote to. You can set up multiple vote receivers and load balance between them,
however this configuration has limited benefit and is probably more work than it
is worth. Instead of communicating through plugin messaging like the plugin
messaging forwarding strategy, proxy forwarding instead acts like a voting
website to each of the backend servers and duplicates the vote it receives to
each of the backend servers it is configured with.
First, we will set up all of the Minecraft servers before setting up the head
vote receiver. For each of the servers, NuVotifier should be configured to bind
to an unused port. This port must be valid. If you have multiple votifiers on
the same IP/server, make sure that the port which you are using between
NuVotifier instances are different. Remember where to find the port number as we
will need it later to insert into the vote receiver Bungeecord NuVotifier
configuration. In addition to the port number, you will also need the default
token - this is the string auto generated by NuVotifier under the `default` key
under `tokens`. Since we are using proxy forwarding, change
`disable-v1-protocol` to true - we will only be using the new NuVotifier
votifier protocol within our network (vote receiver -> minecraft server). Once
all configured, that is all we need to do. The rest of the defaults will work.
Now we will configure the vote receiving NuVotifier Bungeecord server. The port
should be configured as described many times above, being a unique port. Instead
of setting `method` to `pluginMessaging` like above, we will set `method` to
`proxy`. This will tell NuVotifier that we instead want to proxy the votes
instead of forward them through plugin messaging channels.
The last step is to set up the proxy forwaring settings. Under the `proxy`
section is a place to enter each server. Create a unique name for each of the
server's you will be forwarding votes to. The default Bungeecord NuVotifier
configuration already has two Minecraft servers filled in - Hub and Hub2. You
should replace these with your own servers - the names of the servers do not
matter. Under each of these servers, we need to configure `address`, `port`, and
`token`. `address` is the IP address of the Bukkit/Sponge server. This may be
`localhost` or `127.0.0.1` if the Minecraft server is on the same physical
server as the vote receiving NuVotifier server. `port` is the port of the
Bukkit/Sponge NuVotifier server. `token` is the default token of the
Bukkit/Sponge NuVotifier server. This can be found within its configuration,
usually under the `token` section. Once the servers are configured, NuVotifier
will forward all votes it receives to the servers configured under the `proxy`
section.
# Debugging NuVotifier
NuVotifier can be difficult to set up in some configurations. If you find you
don't know what is wrong, you may want to try setting `debug` in each of the
server's NuVotifier configurations to `true`. This will increase logging within
your console.
# Default Configurations
If you find you have messed up NuVotifier beyond repair, you can grab a default
configuration from the following locations (depending on the server it is
installed on):
+ [Bukkit](https://github.com/parallelblock/NuVotifier/blob/master/bukkit/src/main/resources/bukkitConfig.yml)
+ [Sponge](https://github.com/parallelblock/NuVotifier/blob/master/sponge/src/main/resources/com/vexsoftware/votifier/sponge/spongeConfig.yml)
+ [Bungeecord](https://github.com/parallelblock/NuVotifier/blob/master/bungeecord/src/main/resources/bungeeConfig.yml)
You can also regenerate this configuration by deleting the old configuration and
restarting the server.
+16
View File
@@ -0,0 +1,16 @@
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'
}
+2 -2
View File
@@ -1,6 +1,6 @@
#Sat Feb 24 13:39:00 EST 2018
#Thu May 31 03:56:27 EDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
+10
View File
@@ -0,0 +1,10 @@
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,10 +1,8 @@
package com.vexsoftware.votifier.util;
package io.ibj.nuvotifier.legacyserver;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class GsonInst {
public static final Gson gson = new GsonBuilder().create();
}
@@ -1,4 +1,4 @@
package com.vexsoftware.votifier.util;
package io.ibj.nuvotifier.legacyserver;
import java.math.BigInteger;
import java.security.SecureRandom;
@@ -0,0 +1,99 @@
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();
}
}
@@ -0,0 +1,65 @@
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);
}
}
@@ -1,6 +1,5 @@
package com.vexsoftware.votifier.net;
package io.ibj.nuvotifier.legacyserver;
import com.vexsoftware.votifier.util.TokenUtil;
import io.netty.util.AttributeKey;
public class VotifierSession {
@@ -40,8 +39,14 @@ public class VotifierSession {
}
public enum ProtocolVersion {
UNKNOWN,
ONE,
TWO
UNKNOWN("unknown"),
ONE("protocol v1"),
TWO("protocol v2"),
TEST("test");
public final String humanReadable;
ProtocolVersion(String hr) {
this.humanReadable = hr;
}
}
}
@@ -1,15 +1,15 @@
package com.vexsoftware.votifier.net.protocol;
package io.ibj.nuvotifier.legacyserver.protocol;
import com.google.gson.JsonObject;
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.ibj.nuvotifier.legacyserver.GsonInst;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.model.NuVote;
import io.ibj.nuvotifier.platform.VoteHandler;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
public class VoteInboundHandler extends SimpleChannelInboundHandler<NuVote> {
private final VoteHandler handler;
public VoteInboundHandler(VoteHandler handler) {
@@ -17,10 +17,10 @@ public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, final Vote vote) throws Exception {
protected void channelRead0(ChannelHandlerContext ctx, final NuVote vote) throws Exception {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
handler.onVoteReceived(ctx.channel(), vote, session.getVersion());
handler.onVote(vote);
session.completeVote();
if (session.getVersion() == VotifierSession.ProtocolVersion.ONE) {
@@ -33,13 +33,12 @@ public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
handler.onError(ctx.channel(), session.hasCompletedVote(), cause);
handler.onError(cause, session.hasCompletedVote(), ctx.channel().remoteAddress().toString());
if (session.getVersion() == VotifierSession.ProtocolVersion.TWO) {
JsonObject object = new JsonObject();
object.addProperty("status", "error");
object.addProperty("cause", cause.getClass().getSimpleName());
@@ -1,7 +1,6 @@
package com.vexsoftware.votifier.net.protocol;
package io.ibj.nuvotifier.legacyserver.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.net.VotifierSession;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -14,11 +13,9 @@ import java.nio.charset.StandardCharsets;
*/
public class VotifierGreetingHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
public void channelActive(ChannelHandlerContext ctx) {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
VotifierPlugin plugin = ctx.channel().attr(VotifierPlugin.KEY).get();
String version = "VOTIFIER " + plugin.getVersion() + " " + session.getChallenge() + "\n";
String version = "VOTIFIER 2 " + session.getChallenge() + "\n";
ByteBuf versionBuf = Unpooled.copiedBuffer(version, StandardCharsets.UTF_8);
ctx.writeAndFlush(versionBuf);
}
@@ -1,8 +1,8 @@
package com.vexsoftware.votifier.net.protocol;
package io.ibj.nuvotifier.legacyserver.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import io.ibj.nuvotifier.legacyserver.protocol.v1crypto.RSA;
import io.ibj.nuvotifier.model.NuVote;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -15,8 +15,15 @@ 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) throws Exception {
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
if (buf.readableBytes() < 256) {
return;
}
@@ -26,10 +33,8 @@ 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, plugin.getProtocolV1Key().getPrivate());
block = RSA.decrypt(block, keyProvider.getPrivateKey());
} catch (Exception e) {
throw new CorruptedFrameException("Could not decrypt data (is your key correct?)", e);
}
@@ -46,7 +51,7 @@ public class VotifierProtocol1Decoder extends ByteToMessageDecoder {
}
// Create the vote.
Vote vote = new Vote(split[1], split[2], split[3], split[4]);
NuVote vote = NuVote.createLegacy(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 com.vexsoftware.votifier.net.protocol;
package io.ibj.nuvotifier.legacyserver.protocol;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.VotifierPlugin;
import io.ibj.nuvotifier.legacyserver.GsonInst;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.platform.V2TokenProvider;
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,6 +23,12 @@ 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);
@@ -37,16 +43,9 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
throw new CorruptedFrameException("Challenge is not valid");
}
// 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() + "'");
}
}
String serviceName = votePayload.get("serviceName").getAsString();
// Get the key for the given service.
Key key = tokenProvider.getKeyForService(serviceName);
// Verify signature.
String sigHash = voteMessage.get("signature").getAsString();
@@ -61,12 +60,16 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
UUID.fromString(votePayload.get("uuid").getAsString());
}
if (votePayload.get("username").getAsString().length() > 16) {
String username = votePayload.get("username").getAsString();
if (username.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(votePayload);
Vote vote = new Vote(serviceName, username, address, Long.toString(timestamp));
list.add(vote);
ctx.pipeline().remove(this);
@@ -1,6 +1,8 @@
package com.vexsoftware.votifier.net.protocol;
package io.ibj.nuvotifier.legacyserver.protocol;
import com.vexsoftware.votifier.net.VotifierSession;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import io.ibj.nuvotifier.platform.V2TokenProvider;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -18,15 +20,18 @@ 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;
public VotifierProtocolDifferentiator(boolean testMode, boolean allowv1) {
private final V1KeyProvider v1;
private final V2TokenProvider v2;
public VotifierProtocolDifferentiator(V1KeyProvider v1, V2TokenProvider v2, boolean testMode) {
this.testMode = testMode;
this.allowv1 = allowv1;
this.v1 = v1;
this.v2 = v2;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) throws Exception {
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
// Determine the number of bytes that are available.
int readable = buf.readableBytes();
@@ -43,24 +48,28 @@ 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());
ctx.pipeline().addAfter("protocol2StringDecoder", "protocol2VoteDecoder", new VotifierProtocol2Decoder(v2));
ctx.pipeline().addAfter("protocol2VoteDecoder", "protocol2StringEncoder", new StringEncoder(StandardCharsets.UTF_8));
ctx.pipeline().remove(this);
}
} else {
if (!allowv1) {
if (v1 == null) {
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());
ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder(v1));
ctx.pipeline().remove(this);
}
}
@@ -16,7 +16,7 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier.net.protocol.v1crypto;
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
import javax.crypto.Cipher;
import java.security.PrivateKey;
@@ -16,8 +16,9 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier.net.protocol.v1crypto;
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
import java.io.IOException;
import java.util.Base64;
import java.io.File;
import java.io.FileOutputStream;
@@ -59,6 +60,19 @@ public class RSAIO {
}
}
public static byte[] readB64File(File directory, String name) throws IOException {
File f = new File(directory, name);
byte[] contents = Files.readAllBytes(f.toPath());
String strContents = new String(contents);
strContents = strContents.trim();
try {
return Base64.getDecoder().decode(strContents);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Base64 decoding exception: This is probably due to a corrupted file, " +
"but in case it isn't, here is a b64 representation of what we read: " + new String(Base64.getEncoder().encode(contents), StandardCharsets.UTF_8), e);
}
}
/**
* Loads an RSA key pair from a directory. The directory must have the files
* "public.key" and "private.key".
@@ -69,14 +83,10 @@ public class RSAIO {
*/
public static KeyPair load(File directory) throws Exception {
// Read the public key file.
File publicKeyFile = new File(directory + "/public.key");
byte[] encodedPublicKey = Files.readAllBytes(publicKeyFile.toPath());
encodedPublicKey = Base64.getDecoder().decode(new String(encodedPublicKey, StandardCharsets.UTF_8));
byte[] encodedPublicKey = readB64File(directory, "public.key");
// Read the private key file.
File privateKeyFile = new File(directory + "/private.key");
byte[] encodedPrivateKey = Files.readAllBytes(privateKeyFile.toPath());
encodedPrivateKey = Base64.getDecoder().decode(new String(encodedPrivateKey, StandardCharsets.UTF_8));
byte[] encodedPrivateKey = readB64File(directory, "private.key");
// Instantiate and return the key pair.
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
@@ -16,7 +16,7 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier.net.protocol.v1crypto;
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
+3
View File
@@ -0,0 +1,3 @@
# 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.
@@ -0,0 +1,89 @@
/*
* 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;
}
}
@@ -0,0 +1,17 @@
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.
@@ -0,0 +1,7 @@
package io.ibj.nuvotifier.multiplatform;
public interface BackendServer {
String getName();
boolean sendPluginMessage(String channel, byte[] data);
}
@@ -0,0 +1,10 @@
package io.ibj.nuvotifier.multiplatform;
import java.util.Collection;
import java.util.Optional;
public interface ProxyVotifierPlugin {
Collection<BackendServer> getAllBackendServers();
Optional<BackendServer> getServer(String name);
}
+3
View File
@@ -0,0 +1,3 @@
dependencies {
api project(":model")
}
@@ -0,0 +1,43 @@
package io.ibj.nuvotifier.platform;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JavaUtilLogger implements LoggingAdapter {
private final Logger logger;
public JavaUtilLogger(Logger logger) {
this.logger = logger;
}
@Override
public void error(String s) {
logger.severe(s);
}
@Override
public void error(String s, Object... o) {
logger.log(Level.SEVERE, s, o);
}
@Override
public void warn(String s) {
logger.warning(s);
}
@Override
public void warn(String s, Object... o) {
logger.log(Level.WARNING, s, o);
}
@Override
public void info(String s) {
logger.info(s);
}
@Override
public void info(String s, Object... o) {
logger.log(Level.INFO, s, o);
}
}
@@ -0,0 +1,13 @@
package io.ibj.nuvotifier.platform;
public interface LoggingAdapter {
void error(String s);
void error(String s, Object... o);
void warn(String s);
void warn(String s, Object... o);
void info(String s);
void info(String s, Object... o);
}
@@ -0,0 +1,7 @@
package io.ibj.nuvotifier.platform;
import java.security.PrivateKey;
public interface V1KeyProvider {
PrivateKey getPrivateKey();
}
@@ -0,0 +1,7 @@
package io.ibj.nuvotifier.platform;
import java.security.Key;
public interface V2TokenProvider {
Key getKeyForService(String serviceName);
}
@@ -0,0 +1,12 @@
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");
}
}
@@ -0,0 +1,53 @@
package io.ibj.nuvotifier.platform.scheduler;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceVotifierScheduler implements VotifierScheduler {
private final ScheduledExecutorService service;
public ScheduledExecutorServiceVotifierScheduler(ScheduledExecutorService service) {
if (service == null)
throw new NullPointerException("Service is null");
this.service = service;
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return new ScheduledVotifierTaskWrapper(service.submit(runnable));
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new ScheduledVotifierTaskWrapper(service.submit(runnable));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return new ScheduledVotifierTaskWrapper(service.schedule(runnable, delay, unit));
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new ScheduledVotifierTaskWrapper(service.schedule(runnable, delay, unit));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new ScheduledVotifierTaskWrapper(service.scheduleAtFixedRate(runnable, delay, repeat, unit));
}
private static class ScheduledVotifierTaskWrapper implements ScheduledVotifierTask {
private final Future<?> future;
private ScheduledVotifierTaskWrapper(Future<?> future) {
this.future = future;
}
@Override
public void cancel() {
future.cancel(false);
}
}
}
@@ -0,0 +1,5 @@
package io.ibj.nuvotifier.platform.scheduler;
public interface ScheduledVotifierTask {
void cancel();
}
@@ -0,0 +1,15 @@
package io.ibj.nuvotifier.platform.scheduler;
import java.util.concurrent.TimeUnit;
public interface VotifierScheduler {
ScheduledVotifierTask sync(Runnable runnable);
ScheduledVotifierTask onPool(Runnable runnable);
ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit);
ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit);
ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit);
}
-52
View File
@@ -1,52 +0,0 @@
// This file contains special build instructions specifically for the ParallelBlock CI in order to instruct it how to
// build the project. Usually, this shouldn't be interacted with by a human.
import com.mashape.unirest.http.*
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.mashape.unirest:unirest-java:1.4.9'
}
}
subprojects {
apply plugin: "maven-publish"
publishing {
repositories {
maven {
name 'parallelBlock'
if (project.version.endsWith("-SNAPSHOT"))
url "$System.env.SNAPSHOT_REPO"
else
url "$System.env.RELEASE_REPO"
credentials {
username "$System.env.REPO_USERNAME"
password "$System.env.REPO_PASSWORD"
}
}
}
publications {
versioned(MavenPublication) {
from components.java
}
}
}
if (System.env.LATEST_BUILD) {
task publishLatestToParallelBlockRaw {
inputs.files(configurations.archives.artifacts.files)
outputs.upToDateWhen {false}
doLast {
configurations.archives.artifacts.each {
def resp = Unirest.put("$System.env.RAW_UPLOAD_PATH/${it.name}.${it.extension}")
.basicAuth("$System.env.REPO_USERNAME", "$System.env.REPO_PASSWORD")
.body(it.file.bytes).asString()
assert resp.status >= 200 && resp.status <= 299
}
}
}
}
}
+15 -10
View File
@@ -1,14 +1,19 @@
rootProject.name = 'nuvotifier'
include 'common'
findProject(':common')?.name = 'nuvotifier-common'
// The model for a 'Vote'.
include 'model'
include 'bukkit'
findProject(':bukkit')?.name = 'nuvotifier-bukkit'
include 'bungeecord'
findProject(':bungeecord')?.name = 'nuvotifier-bungeecord'
include 'sponge'
findProject(':sponge')?.name = 'nuvotifier-sponge'
// The 'platform' api.
include 'platform'
include 'multiplatform'
// 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'
+4
View File
@@ -0,0 +1,4 @@
dependencies {
api group: "org.spongepowered", name:"spongeapi", version: "7.1.0"
implementation project(":model")
}
-4
View File
@@ -1,4 +0,0 @@
dependencies {
compileOnly group: "org.spongepowered", name: "spongeapi", version: "7.0.0-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -1,392 +0,0 @@
package com.vexsoftware.votifier.sponge;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
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.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAKeygen;
import com.vexsoftware.votifier.sponge.event.VotifierEvent;
import com.vexsoftware.votifier.sponge.forwarding.ForwardedVoteListener;
import com.vexsoftware.votifier.sponge.forwarding.ForwardingVoteSink;
import com.vexsoftware.votifier.sponge.forwarding.SpongePluginMessagingForwardingSink;
import com.vexsoftware.votifier.util.KeyCreator;
import com.vexsoftware.votifier.util.TokenUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
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.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Plugin(id = "nuvotifier", name = "NuVotifier", version = "2.3.7", authors = "ParallelBlock LLC",
description = "Safe, smart, and secure Votifier server plugin")
public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.VotifierPlugin, ForwardedVoteListener {
private Logger logger;
@Inject
@ConfigDir(sharedRoot = false)
private Path baseDirectory;
@Inject
public VotifierPlugin(Logger logger) {
this.logger = logger;
}
@Listener
public void onServerStart(GameStartedServerEvent event) {
// Set the plugin version.
version = this.getClass().getAnnotation(Plugin.class).version();
// Handle configuration.
Path config = baseDirectory.resolve("config.yml");
File rsaDirectory = baseDirectory.resolve("rsa").toFile();
/*
* 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;
Optional<InetSocketAddress> address = Sponge.getServer().getBoundAddress();
if (address.isPresent())
hostAddr = address.get().getAddress().getHostAddress();
else
hostAddr = "0.0.0.0";
/*
* Create configuration file if it does not exists; otherwise, load it
*/
try {
Files.createDirectories(baseDirectory);
} catch (FileAlreadyExistsException ignored) {
} catch (IOException e) {
throw new RuntimeException("Can't create base directory", e);
}
if (!Files.exists(config)) {
try {
// First time run - do some initialization.
logger.info("Configuring Votifier for the first time...");
// Load and manually replace variables in the configuration.
String cfgStr = new String(ByteStreams.toByteArray(this.getClass().getResourceAsStream("spongeConfig.yml")),
StandardCharsets.UTF_8);
String token = TokenUtil.newToken();
cfgStr = cfgStr.replace("%default_token%", token).replace("%ip%", hostAddr);
Files.write(config, ImmutableList.<CharSequence>of(cfgStr), StandardCharsets.UTF_8);
/*
* Remind hosted server admins to be sure they have the right
* port number.
*/
logger.info("------------------------------------------------------------------------------");
logger.info("Assigning NuVotifier to listen on port 8192. If you are hosting Craftbukkit on a");
logger.info("shared server please check with your hosting provider to verify that this port");
logger.info("is available for your use. Chances are that your hosting provider will assign");
logger.info("a different port, which you need to specify in config.yml");
logger.info("------------------------------------------------------------------------------");
logger.info("Your default NuVotifier token is " + token + ".");
logger.info("You will need to provide this token when you submit your server to a voting");
logger.info("list.");
logger.info("------------------------------------------------------------------------------");
} catch (Exception ex) {
logger.error("Error creating configuration file", ex);
gracefulExit();
return;
}
}
// Load configuration.
ConfigurationLoader loader = YAMLConfigurationLoader.builder()
.setPath(config)
.build();
ConfigurationNode node;
try {
node = loader.load();
} catch (IOException e) {
logger.error("Error loading configuration file", e);
gracefulExit();
return;
}
/*
* 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) {
logger.error("Error creating or reading RSA tokens", ex);
gracefulExit();
return;
}
debug = node.getNode("debug").getBoolean(false);
// Load Votifier tokens.
ConfigurationNode tokenSection = node.getNode("tokens");
if (tokenSection.hasMapChildren()) {
for (Map.Entry<Object, ? extends ConfigurationNode> entry : tokenSection.getChildrenMap().entrySet()) {
if (entry.getKey() instanceof String) {
tokens.put((String) entry.getKey(), KeyCreator.createKeyFrom(entry.getValue().getString()));
logger.info("Loaded token for website: " + entry.getKey());
}
}
} else {
String token = TokenUtil.newToken();
tokenSection.setValue(ImmutableMap.of("default", token));
tokens.put("default", KeyCreator.createKeyFrom(token));
try {
loader.save(node);
} catch (IOException e) {
logger.error("Error generating Votifier token", e);
gracefulExit();
return;
}
logger.info("------------------------------------------------------------------------------");
logger.info("No tokens were found in your configuration, so we've generated one for you.");
logger.info("Your default Votifier token is " + token + ".");
logger.info("You will need to provide this token when you submit your server to a voting");
logger.info("list.");
logger.info("------------------------------------------------------------------------------");
}
// Initialize the receiver.
final String host = node.getNode("host").getString(hostAddr);
final int port = node.getNode("port").getInt(8192);
if (debug)
logger.info("DEBUG mode enabled!");
if (port >= 0) {
final boolean disablev1 = node.getNode("disable-v1-protocol").getBoolean(false);
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("------------------------------------------------------------------------------");
}
serverGroup = new NioEventLoopGroup(1);
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(serverGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(com.vexsoftware.votifier.VotifierPlugin.KEY).set(VotifierPlugin.this);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, !disablev1));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(VotifierPlugin.this));
}
})
.bind(host, port)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
serverChannel = future.channel();
logger.info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
} else {
SocketAddress socketAddress = future.channel().localAddress();
if (socketAddress == null) {
socketAddress = new InetSocketAddress(host, port);
}
logger.error("Votifier was not able to bind to " + socketAddress, future.cause());
}
}
});
} 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("------------------------------------------------------------------------------");
}
ConfigurationNode forwardingConfig = node.getNode("forwarding");
if (forwardingConfig != null) {
String method = forwardingConfig.getNode("method").getString("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.getNode("pluginMessaging").getNode("channel").getString("NuVotifier");
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 (serverGroup != null) {
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
}
if (forwardingMethod != null)
forwardingMethod.halt();
logger.info("Votifier disabled.");
}
public Logger getLogger() {
return logger;
}
/**
* The current Votifier version.
*/
private String version;
/**
* The server channel.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
/**
* 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!");
}
/**
* Gets the version.
*
* @return The version
*/
public String getVersion() {
return version;
}
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) throws Exception {
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(new Runnable() {
@Override
public void run() {
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(new Runnable() {
@Override
public void run() {
VotifierEvent event = new VotifierEvent(v, Sponge.getCauseStackManager().getCurrentCause());
Sponge.getEventManager().post(event);
}
})
.submit(this);
}
}
@@ -1,17 +0,0 @@
package com.vexsoftware.votifier.sponge.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);
}
@@ -1,13 +0,0 @@
package com.vexsoftware.votifier.sponge.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();
}
@@ -1,46 +0,0 @@
package com.vexsoftware.votifier.sponge.forwarding;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.sponge.VotifierPlugin;
import com.vexsoftware.votifier.util.GsonInst;
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;
import java.nio.charset.StandardCharsets;
public class SpongePluginMessagingForwardingSink implements ForwardingVoteSink, RawDataListener {
public SpongePluginMessagingForwardingSink(VotifierPlugin p, String channel, ForwardedVoteListener listener) {
this.channelBinding = Sponge.getChannelRegistrar().createRawChannel(p, channel);
this.channelBinding.addListener(Platform.Type.SERVER, this);
this.listener = listener;
this.p = p;
}
private final VotifierPlugin p;
private final ForwardedVoteListener listener;
private final ChannelBinding.RawDataChannel channelBinding;
@Override
public void halt() {
channelBinding.removeListener(this);
}
@Override
public void handlePayload(ChannelBuf channelBuf, RemoteConnection remoteConnection, Platform.Type type) {
try {
byte[] msgDirBuf = channelBuf.readBytes(channelBuf.available());
String message = new String(msgDirBuf, StandardCharsets.UTF_8);
JsonObject jsonObject = GsonInst.gson.fromJson(message, JsonObject.class);
Vote v = new Vote(jsonObject);
listener.onForward(v);
} catch (Exception e) {
p.getLogger().error("There was an unknown error when processing a forwarded vote.", e);
}
}
}
@@ -1,29 +0,0 @@
# 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
# Whether or not to print debug messages. In a production system, this should be set to false.
# This is useful when initially setting up NuVotifier to ensure votes are being delivered.
debug: false
# 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
-35
View File
@@ -1,35 +0,0 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.2'
}
}
apply plugin: 'com.github.johnrengelman.shadow'
dependencies {
compile project(":nuvotifier-bukkit")
compile project(":nuvotifier-bungeecord")
compile project(":nuvotifier-sponge")
}
shadowJar {
baseName = 'nuvotifier'
classifier = null
relocate "io.netty", "com.vexsoftware.votifier.netty"
relocate "org.json", "com.vexsoftware.votifier.json"
relocate "com.google", "com.vexsoftware.votifier.google"
relocate "org.apache.commons.io", "com.vexsoftware.votifier.commons.io"
}
artifacts {
tasks.shadowJar.outputs.files.each {
archives(it) {
builtBy shadowJar
}
}
}
build.dependsOn shadowJar
+4
View File
@@ -0,0 +1,4 @@
dependencies {
api group: "com.velocitypowered", name: "velocity-api", version: "1.0-SNAPSHOT"
implementation project(":model")
}
@@ -0,0 +1,23 @@
package com.vexsoftware.votifier.velocity.event;
import com.vexsoftware.votifier.model.Vote;
import io.ibj.nuvotifier.model.NuVote;
public class VotifierEvent {
private final NuVote vote;
public VotifierEvent(NuVote vote) {
this.vote = vote;
}
public Vote getVote() {
return vote;
}
@Override
public String toString() {
return "VotifierEvent{" +
"vote=" + vote +
'}';
}
}