Compare commits

...
99 Commits
Author SHA1 Message Date
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
Joseph Hirschfeld 3ac45554ac bump to 2.3.7 2018-04-07 12:00:22 -04:00
Joseph Hirschfeld d27f6c5cea Actually add the array to the thing.
Copying and pasting is not an appropriate development strategy.
2018-04-06 01:51:27 -04:00
Joseph Hirschfeld 4d1f7016f6 Add guava as a dependency
Gson requires guava to be a dependency, but it isn't listed explicitly.
For whatever reason..
2018-04-04 20:46:13 -04:00
Joseph Hirschfeld 2673fc2d4d Relocate com.google.* classes
Other plugins may incorrectly supply their own com.google.* classes,
which collide with ours. We don't like that, so we relocate our classes
so they can't touch them like the big meanies they are.
2018-04-03 01:45:09 -04:00
Joseph Hirschfeld 60c787c7f2 point to moss discord instead of parallelblock 2018-04-01 19:47:26 -04:00
Joseph Hirschfeld 713cf90c56 Migrate to GSON
org.json was found to have stack overflow exception issues, could
possibly be security related.
2018-03-31 17:54:17 -04:00
Joseph Hirschfeld eba7ab9c86 Update org.json version 2018-03-30 00:50:37 -04:00
Joseph Hirschfeld d72977325c Add test step to ci build process 2018-03-29 01:00:56 -04:00
Joseph Hirschfeld c3e9a5bac9 Modify error handling in context of votes
If not in debug mode, Nuvotifier will now only log exceptions if we
haven't already sent out the vote as an event. If we have sent out the
vote as an event, then we instead ignore the log.

When in debug mode, the message is now modified specifying that while
there was an exception, be noted that the vote was indeed processed to
the fullest ability to which it could (aka, not our fault!)
2018-03-29 00:57:13 -04:00
Joseph Hirschfeld 161617d636 Add discord server 2018-03-28 21:10:12 -04:00
Joseph Hirschfeld cf95fd1f28 Add missing plugin author and description fields
Sponge suggests adding an author and description to the plugin
annotation.
2018-03-26 22:55:08 -04:00
Joseph Hirschfeld 8f1bf5b4b9 Read in an integer, not a short. Fixes #76
Java does not have unsigned types. As such, too big of short values will
roll over. Account for this with an integer datatype.
2018-03-26 22:53:32 -04:00
Joseph Hirschfeld ee9d3cae42 fix tokenization of plugin versioning 2018-03-13 20:31:40 -04:00
Joseph Hirschfeld e4f910e576 clean up publish script imports 2018-03-13 20:21:58 -04:00
Joseph Hirschfeld 77cf6bf457 Update sponge plugin version 2018-03-13 20:21:04 -04:00
Joseph Hirschfeld 8a72c4d9c7 Fix formatting issues within usage.md, add building section 2018-03-13 01:55:45 -04:00
Joseph Hirschfeld 39ae4ef860 Revamp NuVotifier documentation 2018-03-13 01:51:34 -04:00
Joseph Hirschfeld d2d12a4a86 define inputs and never up to date 2018-03-13 00:16:45 -04:00
Joseph Hirschfeld 5644078997 define shadowJar output as more archives artifacts 2018-03-12 23:34:42 -04:00
Joseph Hirschfeld bf5822282e only publish versioned maven, not latest (removed) 2018-03-12 22:58:07 -04:00
Joseph Hirschfeld 55e883759f Don't publish a 'latest' version
its stupid and gradle doesn't like it, so lets just not
2018-03-12 22:45:44 -04:00
Joseph Hirschfeld ba6a118058 Remove travis 2018-03-12 22:15:23 -04:00
Joseph Hirschfeld 93c0d53f2d Add raw artifact uploading 2018-03-12 22:14:42 -04:00
Joseph Hirschfeld 8d4bdf69b3 add drone build 2018-03-02 22:28:06 -05:00
Joseph Hirschfeld 700be0a04e remove maven, add gradle as build tooling.
add maven publishing
2018-03-02 22:05:03 -05:00
Joseph Hirschfeld bf7c78de46 Negative ports in mc server impls disable the port
Does not work on bungee, as it would result in an invalid configuration
(whats the point of the plugin?)

Updated documentation to finally remove enableExternal, as this is the
official substitute (as its harder to mess up)
2018-02-20 20:51:27 -05:00
Joseph Hirschfeld c0ea57176c Update org.json 2018-01-24 19:50:58 -05:00
Joseph Hirschfeld 65bf833fa7 Fixes #56 Do not rely on direct buffer
Sponge 7.0 doesn't appear to use a direct buffer to back their ByteBuf
objects. Copy out the available data, which we assume is all that is
available.
2018-01-22 00:25:10 -05:00
Joseph Hirschfeld 03beac9ae0 bump to 2.3.7-snapshot 2018-01-22 00:25:10 -05:00
Joseph Hirschfeld 4277dfd74d update to 2.3.6 2018-01-15 22:56:51 -05:00
Joseph Hirschfeld 28b4d8dcd0 Updated for API 7 support 2018-01-15 22:48:53 -05:00
DoNotSpamPls 733204b6b9 votifier.inaptbox.com -> craftservers.net/tools/votifier-tester (#39) 2017-11-08 12:38:10 -05:00
DoNotSpamPls 52956ce210 Fix Travis-CI builds failing (#38) 2017-11-02 18:42:56 -04:00
Joseph Hirschfeld 2de64da511 change version to 2.3.6-pre 2017-11-01 13:43:21 -04:00
Joseph Hirschfeld 0ea6867d91 Add more verbose error handling for bad token.
Fixes #25
2017-11-01 13:38:06 -04:00
Joseph Hirschfeld 190bfaa4a9 Add player centric vote forwarding
Used when 'onlySendToJoinedServer' is true yet 'joinedServerFallback' is
empty, null, or unreachable. Will retransmit vote to the server the
player joins that is not on the ignore list.

Also add FileCache support for the new player cache layout with a new
schema system.
2017-11-01 13:31:10 -04:00
Joseph Hirschfeld b43dda37b2 update netty version
4.0.31.Final -> 4.1.16.Final

Required test modification - somewhere they fixed a bug where releasing
a ByteBuf given to the pipeline should not be released by the sender -
principle property of handing memory off to a methodcall for ownership.
They should really document this stuff better...
2017-11-01 11:11:19 -04:00
Joseph Hirschfeld 4dfb157166 Bump project version
We aren't writing on a solid version!
2017-11-01 10:46:58 -04:00
Joseph Hirschfeld d8c2e072b1 Add a TTL for the file vote cache.
Adds a locally trusted TTL to each vote locally, not using untrusted
remote timestamp. Add day-based ttl to FileVoteCache.

Fixes #15
2017-11-01 10:43:53 -04:00
lifehome 359b616a41 Replace legacy Java EE method for Java 9 - Drop support for Java 7(#35)
* [Test] Java 1.9 patch

Signed-off-by: Ivan Ip <m@lifeho.me>

* Revert version tag
This version must compile and execute on or above Java 9 SE, as the classfile cannot be read by Java 8 or below.

Signed-off-by: Ivan Ip <m@lifeho.me>

* Revert to Java 8 for backward compatibility
This build requires Java 1.8 or up to be compiled or executed.

Signed-off-by: Ivan Ip <m@lifeho.me>

* Fix test cases typo

Signed-off-by: Ivan Ip <m@lifeho.me>

* Revert to 1.8 API as per requested for backward compatibility

Signed-off-by: Ivan Ip <m@lifeho.me>
2017-10-26 21:04:27 -04:00
Tux 9342f75317 2.3.5 2017-05-08 17:27:55 -04:00
Tux f17bb4e414 Remove the enableExternal option.
This one option is the cause of so many support complaints in Discord and on our forums. As vote forwarding using proxying is now the norm, removing this saves us time and effort down the road.
2017-05-08 17:25:54 -04:00
Tux 5f347e538b Closes #24, fixes #21 2017-04-25 19:53:08 -04:00
Tux b4fb8c69a9 Go represent! 2017-02-26 11:15:15 -05:00
Tux 95ebcf22a8 ibj.io/v -> votifier.inaptbox.com 2017-02-26 05:57:34 -05:00
Tux 302da3c907 Integrated Bintray 2017-02-26 05:46:09 -05:00
Tux f8019debd0 Releasing 2.3.4 2017-02-19 21:48:59 -05:00
Tux ab317e76b3 Catch the correct exception 2017-02-19 21:40:46 -05:00
Tux 15556be265 Better loading code 2017-02-19 21:39:08 -05:00
Tux 26958e35fe Don't attempt to "deduplicate" cached votes.
This is in line with original Votifier behavior.
2017-01-15 18:26:19 -05:00
Tux e905881238 Log successful vote attempt IPs 2017-01-15 02:44:43 -05:00
Tux fde7a941b6 Update usage document 2016-12-19 18:49:23 -05:00
Tux 9d24deabe9 Temporarily alleviate issues with large file caches
See #14
2016-12-17 21:32:19 -05:00
Tux c2f331e7ec Updated dependencies.
Bukkit 1.7.2 -> Spigot 1.8.8
SpongeAPI 4.1 -> SpongeAPI 5.0.0
2016-12-08 23:01:32 -05:00
Joseph Hirschfeld 705bddd062 Update sponge API version to 4.1.0 2016-08-15 19:40:48 -04:00
Joseph Hirschfeld 826609db30 Update nuvotifer to 2.3.3 2016-08-15 19:27:11 -04:00
Joseph Hirschfeld 0eca96bd67 Fix null file vote cache issues 2016-08-15 19:20:03 -04:00
Tux f767387840 Implement constant-time comparison 2016-06-25 11:32:19 -04:00
Tux 8ae3c21a23 Read key with UTF8 encoding 2016-06-25 11:30:11 -04:00
Tux be1c056502 Try fixing onlineServer forwarding again 2016-04-03 20:01:35 -04:00
Tux 500142eb5b add validation 2016-03-31 22:11:02 -04:00
Tux 1e5876bc08 Use try-finally 2016-03-26 23:57:00 -04:00
Tux 9355d6015f fix #7 2016-03-24 15:50:06 -04:00
Tux 700db6dca0 Add standalone server support 2016-03-22 21:41:04 -04:00
Tux f61ba7a6ad fix depend 2016-03-08 08:09:16 -05:00
Tux fe7e8cef41 SpongeAPI 4.x 2016-03-08 08:07:22 -05:00
Tux 7cda39ad12 fix travis 2016-03-05 03:24:37 -05:00
Tux b377a53de4 Fix vote detection 2016-03-05 01:15:57 -05:00
Tux 9a306365b8 2.3.2.2 2016-03-05 00:50:39 -05:00
Tux f74cdbe648 Read and drain the full vote buffer 2016-02-28 21:37:06 -05:00
Tux 6a51c1c106 Don't accept > 16 character usernames in v2 2016-02-28 00:09:31 -05:00
Tux 36195be292 Repermit long usernames since some voting sites still insist on sending them
Votifier v2 votes will not accept them, however.
2016-02-28 00:08:26 -05:00
Tux 893fe93ab6 Document hmacEqual() 2016-01-29 21:46:37 -05:00
Tux 90e844b082 formatting tweak 2016-01-29 21:43:03 -05:00
Joseph Hirschfeld 886b60b2de Only send info when in debug mode 2016-01-29 13:40:15 -05:00
Joseph Hirschfeld 30d8e9884c Use 'token' instead of 'key' to avoid confusion between v1 public keys and v2 tokens. 2016-01-29 01:36:03 -05:00
Joseph Hirschfeld 7e05b0752b Add actual link to usage document 2016-01-28 22:38:35 -05:00
Joseph Hirschfeld e124c371b3 Add build notification icon 2016-01-28 22:37:01 -05:00
Joseph Hirschfeld 4af4ae1006 Add additional logging when the votifier port is opened 2016-01-28 22:31:07 -05:00
Tux b63c4a7c68 Make v1 validation a bit smarter in an attempt to accept votes from brain-damaged libraries 2016-01-28 21:26:32 -05:00
Tux 3cf4741cbb Whoops 2016-01-24 23:59:51 -05:00
Tux bb6c311e2f Can only compile with 1.8 because Sponge requires 1.8 2016-01-24 22:09:31 -05:00
97 changed files with 3318 additions and 1552 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
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
+7 -2
View File
@@ -11,13 +11,14 @@
/build.xml
# maven
/target
/*/target/
dependency-reduced-pom.xml
# vim
.*.sw[a-p]
# various other potential build files
/build
build/
/bin
/dist
/manifest.mf
@@ -42,3 +43,7 @@ debug.log
# Doxygen
/docs/html
debug.txt
# gradle stuff
.gradle/
.gradletasknamecache
-13
View File
@@ -1,13 +0,0 @@
sudo: false
language: java
jdk:
- openjdk7
- oraclejdk7
- oraclejdk8
deploy:
provider: releases
api_key:
secure: j8y4qeDOXAYjmf6WU6EbpUA6AKTz26yJ4zNb6oAuB4h9Y3PpBRjFjCIBL6jJQnMxZMLjdgk72/3uqS7+//jJ8/iiY/OJhO2L+2us8TJDcQhH5EI1USe/lcZKf60iJjmd9Q9pjh1eHNYhxr6kyNLnpa6fw5b/hIBT6CvNZAJV1qbhRpHwtzbgdlMFaHGnpX192vJNw7E3uuJuSwue5XNK2ybDp81oTrM0Q16JCq/UsQhGq4Ba3JffOhdBhpfg4EI2jJfe3vN8CFqojCYKNSp7eL6DPFPaV4x8Ly8cRIOZ5Zb60m5SyWAcagPKJuUq3ZOTHfeVTjkI9ymZMHe1pla2Mq6eitIBwZIGnonjyMxcTdkR9EJgq9jo+1PzdStR/NlGLakBiwPzk8Ov0DZ+iOwpYUkISUkMU0UgKWwoFoRu18CHtp+ALEsoYZhBQGlcShq+4EGv9VMNBYk9vN3KXNRMVGKqXd6O3T6shvSQgoUn/3mvksOTAuhS5J4U3woHecDrXwl/RfP2aqiHhX/S7p7yY4hVcHu8kiSYpNQMbay+rNd6HxFcRe/U9HOUt6kW8/yo1ROyCxs3ze+N6LtGDHspgzkFaay6I6wMHYNDnzzRP4MC3Zc73zxViXol6dGBk4Tawsm16VaI6b1uyWLBGKvuU23azkmMTHUmGFXl2/BTaYw=
file: universal/target/nuvotifier-universal-2.3.2.jar
on:
tags: true
+35
View File
@@ -0,0 +1,35 @@
# 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.
## Why fork Votifier?
Votifier has lagged in activity, has severe security flaws in its protocol, and
is vulnerable to denial of service attacks.
## Is NuVotifier a drop-in replacement for Votifier?
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.
# Documentation / Configuration 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)
# License
NuVotifier is GNU GPLv3 licensed. This project's license can be viewed
[here](LICENSE).
+37
View File
@@ -0,0 +1,37 @@
import org.apache.tools.ant.filters.ReplaceTokens
allprojects {
group 'com.vexsoftware'
version '2.4.1'
}
subprojects {
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven {
url 'https://repo.parallelblock.com/repository/maven-public/'
}
maven {
url 'https://hub.spigotmc.org/nexus/content/groups/public/'
}
maven {
url "https://oss.sonatype.org/content/groups/public/"
}
maven {
url "http://repo.spongepowered.org/maven"
}
}
processResources {
filter(ReplaceTokens, tokens: ["app.version": this.project.version])
}
}
if (System.env.PUBLISH) {
apply from: 'publish.gradle'
}
+5
View File
@@ -0,0 +1,5 @@
dependencies {
compileOnly group: "org.spigotmc", name: "spigot-api", version: "1.8.8-R0.1-SNAPSHOT"
compile project(":nuvotifier-common")
}
-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nuvotifier</artifactId>
<groupId>com.vexsoftware</groupId>
<version>2.3.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nuvotifier-bukkit</artifactId>
<build>
<plugins>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>maven-replacer-plugin</artifactId>
<version>1.3.8</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<file>target/classes/plugin.yml</file>
<replacements>
<replacement>
<token>maven-version-number</token>
<value>${project.version}</value>
</replacement>
</replacements>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-common</artifactId>
<version>${project.parent.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.7.2-R0.1</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,57 @@
package com.vexsoftware.votifier;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import org.bukkit.scheduler.BukkitTask;
import java.util.concurrent.TimeUnit;
class BukkitScheduler implements VotifierScheduler {
private final NuVotifierBukkit plugin;
public BukkitScheduler(NuVotifierBukkit plugin) {
this.plugin = plugin;
}
private int toTicks(int time, TimeUnit unit) {
return (int) (unit.toMillis(time) / 50);
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTask(plugin, runnable));
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskAsynchronously(plugin, runnable));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskLater(plugin, runnable, toTicks(delay, unit)));
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, runnable, toTicks(delay, unit)));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, runnable, toTicks(delay, unit), toTicks(repeat, unit)));
}
private static class BukkitTaskWrapper implements ScheduledVotifierTask {
private final BukkitTask task;
private BukkitTaskWrapper(BukkitTask task) {
this.task = task;
}
@Override
public void cancel() {
task.cancel();
}
}
}
@@ -25,31 +25,29 @@ 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.VotifierServerBootstrap;
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.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
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.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.impl.JDK14LoggerFactory;
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;
@@ -65,6 +63,8 @@ import java.util.logging.Level;
*/
public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, VotifierPlugin, ForwardedVoteListener {
private static final ILoggerFactory FACTORY = new JDK14LoggerFactory();
/**
* The Votifier instance.
*/
@@ -76,14 +76,9 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
private String version;
/**
* The server channel.
* The server bootstrap.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
@@ -101,22 +96,18 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
private Map<String, Key> tokens = new HashMap<>();
private ForwardingVoteSink forwardingMethod;
private VotifierScheduler scheduler;
private Logger pluginLogger;
@Override
public void onEnable() {
NuVotifierBukkit.instance = this;
// Set the plugin version.
version = getDescription().getVersion();
private boolean loadAndBind() {
scheduler = new BukkitScheduler(this);
pluginLogger = FACTORY.getLogger(getLogger().getName());
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
@@ -143,7 +134,7 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
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);
Files.asCharSink(config, StandardCharsets.UTF_8).write(cfgStr);
/*
* Remind hosted server admins to be sure they have the right
@@ -161,11 +152,13 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
getLogger().info("------------------------------------------------------------------------------");
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Error creating configuration file", ex);
gracefulExit();
return;
return false;
}
}
YamlConfiguration cfg;
File rsaDirectory = new File(getDataFolder() + "/rsa");
// Load configuration.
cfg = YamlConfiguration.loadConfiguration(config);
@@ -184,51 +177,46 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
} catch (Exception ex) {
getLogger().log(Level.SEVERE,
"Error reading configuration file or RSA tokens", ex);
gracefulExit();
return;
return false;
}
debug = cfg.getBoolean("debug", false);
boolean setUpPort = cfg.getBoolean("enableExternal", true); //Always default to running the external port
// Load Votifier tokens.
ConfigurationSection tokenSection = cfg.getConfigurationSection("tokens");
if (setUpPort) {
// 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("------------------------------------------------------------------------------");
if (tokenSection != null) {
Map<String, Object> websites = tokenSection.getValues(false);
for (Map.Entry<String, Object> website : websites.entrySet()) {
tokens.put(website.getKey(), KeyCreator.createKeyFrom(website.getValue().toString()));
getLogger().info("Loaded token for website: " + website.getKey());
}
} else {
String token = TokenUtil.newToken();
tokenSection = cfg.createSection("tokens");
tokenSection.set("default", token);
tokens.put("default", KeyCreator.createKeyFrom(token));
try {
cfg.save(config);
} catch (IOException e) {
getLogger().log(Level.SEVERE,
"Error generating Votifier token", e);
return false;
}
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("No tokens were found in your configuration, so we've generated one for you.");
getLogger().info("Your default Votifier token is " + token + ".");
getLogger().info("You will need to provide this token when you submit your server to a voting");
getLogger().info("list.");
getLogger().info("------------------------------------------------------------------------------");
}
// Initialize the receiver.
final String host = cfg.getString("host", hostAddr);
final int port = cfg.getInt("port", 8192);
if (debug)
getLogger().info("DEBUG mode enabled!");
// 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("------------------------------------------------------------------------------");
@@ -237,39 +225,14 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
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.");
} 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());
}
}
});
this.bootstrap = new VotifierServerBootstrap(host, port, this, disablev1);
this.bootstrap.start(error -> {});
} else {
getLogger().info("You have enableExternal set to false in your config.yml. NuVotifier will NOT listen to votes coming in from an external voting list.");
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");
@@ -289,22 +252,70 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
getLogger().severe("No vote forwarding method '" + method + "' known. Defaulting to noop implementation.");
}
}
return true;
}
private void halt() {
// Shut down the network handlers.
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null) {
forwardingMethod.halt();
forwardingMethod = null;
}
}
@Override
public void onEnable() {
NuVotifierBukkit.instance = this;
// Set the plugin version.
version = getDescription().getVersion();
if (!loadAndBind()) {
gracefulExit();
setEnabled(false); // safer to just bomb out
}
}
@Override
public void onDisable() {
// Shut down the network handlers.
if (serverGroup != null) {
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
}
if (forwardingMethod != null) {
forwardingMethod.halt();
}
halt();
getLogger().info("Votifier disabled.");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof ConsoleCommandSender) {
reload();
} else {
sender.sendMessage(ChatColor.RED + "For security and stability, only console may run this command!");
}
return true;
}
private void reload() {
try {
halt();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On halt, an exception was thrown. This may be fine!", ex);
}
if (loadAndBind()) {
getLogger().info("Reload was successful.");
} else {
try {
halt();
getLogger().log(Level.SEVERE, "On reload, there was a problem with the configuration. Votifier currently does nothing!");
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On reload, there was a problem loading, and we could not re-halt the server. Votifier is in an unstable state!", ex);
}
}
}
private void gracefulExit() {
getLogger().log(Level.SEVERE, "Votifier did not initialize properly!");
}
@@ -318,13 +329,14 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
return instance;
}
/**
* Gets the version.
*
* @return The version
*/
public String getVersion() {
return version;
@Override
public Logger getPluginLogger() {
return pluginLogger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
@@ -342,27 +354,27 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
}
@Override
public void onVoteReceived(final Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
getLogger().info("Got a protocol v1 vote record -> " + vote);
getLogger().info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
getLogger().info("Got a protocol v2 vote record -> " + vote);
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));
}
});
Bukkit.getScheduler().runTask(this, () -> Bukkit.getPluginManager().callEvent(new VotifierEvent(vote)));
}
@Override
public void onError(Channel channel, Throwable throwable) {
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress(), throwable);
} else {
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());
}
}
@@ -372,11 +384,6 @@ public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, Votifie
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));
}
});
Bukkit.getScheduler().runTask(this, () -> Bukkit.getPluginManager().callEvent(new VotifierEvent(v)));
}
}
@@ -1,13 +1,14 @@
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 org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
@@ -38,7 +39,7 @@ public class BukkitPluginMessagingForwardingSink implements ForwardingVoteSink,
public void onPluginMessageReceived(String s, Player player, byte[] bytes) {
try {
String message = new String(bytes, StandardCharsets.UTF_8);
JSONObject jsonObject = new JSONObject(message);
JsonObject jsonObject = GsonInst.gson.fromJson(message, JsonObject.class);
Vote v = new Vote(jsonObject);
listener.onForward(v);
} catch (Exception e) {
+1 -5
View File
@@ -9,10 +9,6 @@ port: 8192
# This is useful when initially setting up NuVotifier to ensure votes are being delivered.
debug: false
# Setting this value to false will turn off the votifier port listening for external votes. This is beneficial if the server
# is only listening for votes coming from NuVotifier running on your BungeeCord.
enableExternal: true
# 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.
@@ -30,4 +26,4 @@ forwarding:
# - pluginMessaging - Sets up plugin messaging
method: none
pluginMessaging:
channel: NuVotifier
channel: nuvotifier:votes
+10 -2
View File
@@ -1,5 +1,13 @@
name: Votifier
main: com.vexsoftware.votifier.NuVotifierBukkit
version: maven-version-number
version: @app.version@
description: A plugin that gets notified when votes are made for the server on toplists.
authors: [blakeman8192, Kramer, tuxed]
authors: [ParallelBlock, blakeman8192, Kramer, tuxed]
api-version: 1.13
commands:
nvreload:
description: Reloads the NuVotifier configuration
permission: nuvotifier.console
permission-message: "You do not have permission to run this command."
usage: "/nvreload"
+4
View File
@@ -0,0 +1,4 @@
dependencies {
compileOnly group: "net.md-5", name: "bungeecord-api", version: "1.8-SNAPSHOT"
compile project(":nuvotifier-common")
}
-65
View File
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nuvotifier</artifactId>
<groupId>com.vexsoftware</groupId>
<version>2.3.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nuvotifier-bungeecord</artifactId>
<build>
<plugins>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>maven-replacer-plugin</artifactId>
<version>1.3.8</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<file>target/classes/bungee.yml</file>
<replacements>
<replacement>
<token>maven-version-number</token>
<value>${project.version}</value>
</replacement>
</replacements>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.8-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-common</artifactId>
<version>${project.parent.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,28 @@
package com.vexsoftware.votifier.bungee;
import com.google.common.base.Preconditions;
import com.vexsoftware.votifier.platform.BackendServer;
import net.md_5.bungee.api.config.ServerInfo;
public class BungeeBackendServer implements BackendServer {
private final ServerInfo info;
public BungeeBackendServer(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}
@Override
public String getName() {
return info.getName();
}
@Override
public boolean sendPluginMessage(String channel, byte[] data) {
return info.sendData(channel, data, false);
}
@Override
public String toString() {
return info.getName();
}
}
@@ -0,0 +1,53 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import java.util.concurrent.TimeUnit;
class BungeeScheduler implements VotifierScheduler {
private final NuVotifier plugin;
public BungeeScheduler(NuVotifier plugin) {
this.plugin = plugin;
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return onPool(runnable);
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().runAsync(plugin, runnable));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return delayedOnPool(runnable, delay, unit);
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().schedule(plugin, runnable, delay, unit));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().schedule(plugin, runnable, delay, repeat, unit));
}
private static class BungeeTaskWrapper implements ScheduledVotifierTask {
private final ScheduledTask task;
private BungeeTaskWrapper(ScheduledTask task) {
this.task = task;
}
@Override
public void cancel() {
this.task.cancel();
}
}
}
@@ -4,67 +4,57 @@ 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.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.platform.ProxyVotifierPlugin;
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.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.support.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.MemoryVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.support.forwarding.proxy.ProxyForwardingVoteSource;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.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.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import org.slf4j.Logger;
import org.slf4j.impl.DirtyTricks;
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.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
public class NuVotifier extends Plugin implements VoteHandler, ProxyVotifierPlugin {
/**
* The server channel.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
@@ -86,8 +76,12 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
*/
private ForwardingVoteSource forwardingMethod;
@Override
public void onEnable() {
private VotifierScheduler scheduler;
private Logger pluginLogger;
private void loadAndBind() {
scheduler = new BungeeScheduler(this);
pluginLogger = DirtyTricks.getLogger(getLogger());
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
@@ -108,7 +102,7 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
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);
Files.asCharSink(config, StandardCharsets.UTF_8).write(cfgStr);
/*
* Remind hosted server admins to be sure they have the right
@@ -197,41 +191,9 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
}
// 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.");
} 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());
}
}
});
}
FutureTask<?> initTask = new FutureTask<>(Executors.callable(() -> {
this.bootstrap = new VotifierServerBootstrap(host, port, NuVotifier.this, disablev1);
this.bootstrap.start(err -> {});
}));
getProxy().getScheduler().runAsync(this, initTask);
try {
@@ -255,8 +217,10 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
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")));
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);
}
@@ -292,65 +256,113 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
getLogger().info("Address " + section.getString("address") + " couldn't be looked up. Ignoring!");
continue;
}
ProxyForwardingVoteSource.BackendServer server = new ProxyForwardingVoteSource.BackendServer(s,
new InetSocketAddress(address, section.getShort("port")), KeyCreator.createKeyFrom(section.getString("key")));
serverList.add(server);
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);
forwardingMethod = new ProxyForwardingVoteSource(this, bootstrap::client, serverList, null);
getLogger().info("Forwarding votes from this NuVotifier instance to another NuVotifier server.");
} else {
getLogger().severe("No vote forwarding method '" + fwdMethod + "' known. Defaulting to noop implementation.");
}
}
@Override
public void onDisable() {
public void onEnable() {
loadAndBind();
getProxy().getPluginManager().registerCommand(this, new Command("nvreload") {
@Override
public void execute(CommandSender sender, String[] args) {
if (sender instanceof ProxiedPlayer)
sender.sendMessage(ChatColor.RED + "For security and stability, only console may run this command!");
else
reload();
}
});
}
private void halt() {
// Shut down the network handlers.
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null) {
forwardingMethod.halt();
forwardingMethod = null;
}
}
public void reload() {
try {
halt();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On halt, an exception was thrown. This may be fine!", ex);
}
try {
loadAndBind();
getLogger().info("Reload was successful.");
} catch (Exception ex) {
try {
halt();
getLogger().log(Level.SEVERE, "On reload, there was a problem with the configuration. Votifier currently does nothing!", ex);
} catch (Exception ex2) {
getLogger().log(Level.SEVERE, "On reload, there was a problem loading, and we could not re-halt the server. Votifier is in an unstable state!", ex);
getLogger().log(Level.SEVERE, "(halt exception)", ex2);
}
}
}
@Override
public void onDisable() {
halt();
getLogger().info("Votifier disabled.");
}
@Override
public void onVoteReceived(final Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
getLogger().info("Got a protocol v1 vote record -> " + vote);
getLogger().info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
getLogger().info("Got a protocol v2 vote record -> " + vote);
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));
}
});
getProxy().getScheduler().runAsync(this, () -> getProxy().getPluginManager().callEvent(new VotifierEvent(vote)));
if (forwardingMethod != null) {
getProxy().getScheduler().runAsync(this, new Runnable() {
@Override
public void run() {
forwardingMethod.forward(vote);
}
});
getProxy().getScheduler().runAsync(this, () -> forwardingMethod.forward(vote));
}
}
@Override
public void onError(Channel channel, Throwable throwable) {
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress(), throwable);
} else {
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());
}
}
@@ -366,11 +378,29 @@ public class NuVotifier extends Plugin implements VoteHandler, VotifierPlugin {
}
@Override
public String getVersion() {
return getDescription().getVersion();
public Logger getPluginLogger() {
return pluginLogger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
return debug;
}
@Override
public Collection<BackendServer> getAllBackendServers() {
return getProxy().getServers().values().stream()
.map(BungeeBackendServer::new)
.collect(Collectors.toList());
}
@Override
public Optional<BackendServer> getServer(String name) {
ServerInfo info = getProxy().getServerInfo(name);
return Optional.ofNullable(info).map(BungeeBackendServer::new);
}
}
@@ -0,0 +1,57 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.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 && p.getServer() != null) {
if (forwardSpecific(new BungeeBackendServer(p.getServer().getInfo()), v)) {
return;
}
}
ServerInfo serverInfo = ProxyServer.getInstance().getServers().get(fallbackServer);
// nowhere to fall back to, yet still not online. lets save this vote yet!
if (serverInfo == null)
attemptToAddToPlayerCache(v, v.getUsername());
else if (!forwardSpecific(new BungeeBackendServer(serverInfo), v))
attemptToAddToCache(v, fallbackServer);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent e) {
if (e.getTag().equals(channel)) e.setCancelled(true);
}
@EventHandler
public void onServerConnected(final ServerConnectedEvent e) { //Attempt to resend any votes that were previously cached.
BackendServer server = new BungeeBackendServer(e.getServer().getInfo());
handlePlayerSwitch(server, e.getPlayer().getName());
onServerConnect(server);
}
}
@@ -0,0 +1,34 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
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.
onServerConnect(new BungeeBackendServer(e.getServer().getInfo()));
}
}
@@ -1,47 +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.event.EventHandler;
/**
* @author Joseph Hirschfeld
* @date 12/31/2015
*/
public final class OnlineForwardPluginMessagingForwardingSource extends PluginMessagingForwardingSource {
public OnlineForwardPluginMessagingForwardingSource(String channel, NuVotifier nuVotifier, VoteCache cache, String fallbackServer) {
super(channel, nuVotifier, cache);
this.fallbackServer = fallbackServer;
}
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);
if (serverInfo == null || !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);
}
}
@@ -1,123 +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.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Created by Joe Hirschfeld on 10/20/2015.
*/
public class PluginMessagingForwardingSource implements ForwardingVoteSource, Listener {
public PluginMessagingForwardingSource(String channel, List<String> ignoredServers, NuVotifier nuVotifier, VoteCache cache) {
ProxyServer.getInstance().registerChannel(channel);
ProxyServer.getInstance().getPluginManager().registerListener(nuVotifier, this);
this.channel = channel;
this.nuVotifier = nuVotifier;
this.cache = cache;
this.ignoredServers = ignoredServers;
}
protected PluginMessagingForwardingSource(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);
}
}
}
@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);
}
protected void handleServerConnected(final ServerConnectedEvent e) {
if (cache == null) return;
final String serverName = e.getServer().getInfo().getName();
final Collection<Vote> cachedVotes = cache.evict(serverName);
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(e.getServer().getInfo(), v)) {
evicted++;
} else {
// Re-add to cache to send later.
cache.addToCache(v, serverName);
unsuccessfulEvictions++;
}
}
if (nuVotifier.isDebug()) {
nuVotifier.getLogger().info("Successfully evicted " + evicted + " votes to server '" + serverName + "'.");
if (unsuccessfulEvictions > 0) {
nuVotifier.getLogger().info("Held " + unsuccessfulEvictions + " votes for server '" + serverName + "'.");
}
}
}
}, 1, TimeUnit.SECONDS);
}
}
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);
}
}
@@ -1,87 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.cache;
import com.google.common.io.Files;
import com.vexsoftware.votifier.model.Vote;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class FileVoteCache extends MemoryVoteCache {
private final File cacheFile;
private final ScheduledTask saveTask;
public FileVoteCache(int initialMemorySize, final Plugin plugin, File cacheFile) throws IOException {
super(initialMemorySize);
this.cacheFile = cacheFile;
load();
saveTask = ProxyServer.getInstance().getScheduler().schedule(plugin, new Runnable() {
@Override
public void run() {
try {
save();
} catch (IOException e) {
plugin.getLogger().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 {
if (cacheFile.exists()) {
// Load the cache from disk
JSONObject object = new JSONObject(Files.toString(cacheFile, StandardCharsets.UTF_8));
// Deserialize all votes contained
for (Object server : object.keySet()) {
JSONArray voteArray = object.optJSONArray(((String) server));
if (voteArray == null) continue;
Set<Vote> votes = new LinkedHashSet<>(voteArray.length());
for (int i = 0; i < voteArray.length(); i++) {
votes.add(new Vote(voteArray.getJSONObject(i)));
}
voteCache.put(((String) server), votes);
}
} else {
cacheFile.createNewFile();
}
}
public void save() throws IOException {
cacheLock.lock();
// Create a copy of the votes.
JSONObject votesObject = new JSONObject();
for (Map.Entry<String, Collection<Vote>> entry : voteCache.entrySet()) {
JSONArray array = new JSONArray();
for (Vote vote : entry.getValue()) {
array.put(vote.serialize());
}
votesObject.put(entry.getKey(), array);
}
cacheLock.unlock();
try (BufferedWriter writer = Files.newWriter(cacheFile, StandardCharsets.UTF_8)) {
votesObject.write(writer);
}
}
public void halt() throws IOException {
saveTask.cancel();
save();
}
}
@@ -1,49 +0,0 @@
package com.vexsoftware.votifier.bungee.forwarding.cache;
import com.google.common.collect.ImmutableSet;
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);
}
protected final Map<String, Collection<Vote>> voteCache;
protected final ReentrantLock cacheLock = new ReentrantLock();
@Override
public Collection<String> getCachedServers() {
cacheLock.lock();
Set<String> servers = ImmutableSet.copyOf(voteCache.keySet());
cacheLock.unlock();
return servers;
}
@Override
public void addToCache(Vote v, String server) {
cacheLock.lock();
Collection<Vote> voteCollection = voteCache.get(server);
if (voteCollection == null) {
voteCollection = new LinkedHashSet<>();
voteCache.put(server, voteCollection);
}
voteCollection.add(v);
cacheLock.unlock();
}
@Override
public Collection<Vote> evict(String server) {
cacheLock.lock();
Collection<Vote> fromCollection = voteCache.remove(server);
cacheLock.unlock();
return fromCollection != null ? ImmutableSet.copyOf(fromCollection) : ImmutableSet.<Vote>of();
}
}
@@ -0,0 +1,13 @@
package org.slf4j.impl;
import org.slf4j.Logger;
public class DirtyTricks {
private DirtyTricks() {
throw new AssertionError();
}
public static Logger getLogger(java.util.logging.Logger source) {
return new JDK14LoggerAdapter(source);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
name: NuVotifier
main: com.vexsoftware.votifier.bungee.NuVotifier
version: maven-version-number
author: blakeman8192, Kramer, tuxed
version: @app.version@
author: ParallelBlock, blakeman8192, Kramer, tuxed
@@ -26,7 +26,7 @@ forwarding:
# - proxy - Proxies votes to other NuVotifier servers from this server.
method: none
pluginMessaging:
channel: NuVotifier
channel: nuvotifier:votes
#Only send votes to certain servers. If a server is present on excludedServers, then votes will NOT be forwarded
#to them. If excludedServers is not present, votes will be sent to all servers.
@@ -53,13 +53,15 @@ forwarding:
# 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
key: MyKeyHere
token: MyKeyHere
Hub2:
address: 127.0.0.1
port: 8194
key: AnotherKeyHere
token: AnotherKeyHere
+9
View File
@@ -0,0 +1,9 @@
dependencies {
compile group: "io.netty", name: "netty-handler", version: "4.1.16.Final"
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.google.guava:guava:24.1-jre'
compile 'org.slf4j:slf4j-api:1.7.25'
compile 'org.slf4j:slf4j-jdk14:1.7.25'
testCompile group: "junit", name: "junit", version: "4.12"
testCompile group: "org.json", name: "json", version: "20180130" // retain this for testing reasons
}
-39
View File
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nuvotifier</artifactId>
<groupId>com.vexsoftware</groupId>
<version>2.3.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nuvotifier-common</artifactId>
<dependencies>
<!-- Netty Dependency -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.0.31.Final</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<!-- JSON Dependency -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20140107</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<!-- JUnit, for tests -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -5,7 +5,16 @@ import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.channel.Channel;
public interface VoteHandler {
void onVoteReceived(Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception;
void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception;
void onError(Channel channel, Throwable throwable);
// 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();
}
@@ -18,7 +18,8 @@
package com.vexsoftware.votifier.model;
import org.json.JSONObject;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* A model for a vote.
@@ -47,36 +48,50 @@ public class 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) {
if (username.length() > 16) {
throw new IllegalArgumentException("Username is too long.");
}
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(JSONObject object) {
private static String getTimestamp(JsonElement object) {
try {
return Long.toString(object.getLong("timestamp"));
return Long.toString(object.getAsLong());
} catch (Exception e) {
return object.getString("timestamp");
return object.getAsString();
}
}
public Vote(JSONObject jsonObject) {
this(jsonObject.getString("serviceName"), jsonObject.getString("username"), jsonObject.getString("address"), getTimestamp(jsonObject));
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 + ")";
+ " address:" + address + " timeStamp:" + timeStamp
+ " localTimestamp:" + localTimestamp + ")";
}
/**
@@ -105,9 +120,6 @@ public class Vote {
*/
@Deprecated
public void setUsername(String username) {
if (username.length() > 16) {
throw new IllegalArgumentException("Username is too long.");
}
this.username = username;
}
@@ -158,12 +170,22 @@ public class Vote {
return timeStamp;
}
public JSONObject serialize() {
JSONObject ret = new JSONObject();
ret.put("serviceName", serviceName);
ret.put("username", username);
ret.put("address", address);
ret.put("timestamp", 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;
}
@@ -178,7 +200,6 @@ public class Vote {
if (!username.equals(vote.username)) return false;
if (!address.equals(vote.address)) return false;
return timeStamp.equals(vote.timeStamp);
}
@Override
@@ -0,0 +1,90 @@
package com.vexsoftware.votifier.net;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.vexsoftware.votifier.net.protocol.VoteInboundHandler;
import com.vexsoftware.votifier.net.protocol.VotifierGreetingHandler;
import com.vexsoftware.votifier.net.protocol.VotifierProtocolDifferentiator;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Objects;
import java.util.function.Consumer;
public class VotifierServerBootstrap {
private final String host;
private final int port;
private final NioEventLoopGroup bossLoopGroup;
private final NioEventLoopGroup eventLoopGroup;
private final VotifierPlugin plugin;
private final boolean v1Disable;
private Channel serverChannel;
public VotifierServerBootstrap(String host, int port, VotifierPlugin plugin, boolean v1Disable) {
this.host = host;
this.port = port;
this.plugin = plugin;
this.v1Disable = v1Disable;
this.bossLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO boss").build());
this.eventLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO worker").build());
}
public void start(Consumer<Throwable> error) {
Objects.requireNonNull(error, "error");
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(bossLoopGroup, eventLoopGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(VotifierPlugin.KEY).set(plugin);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, !v1Disable));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(plugin));
}
})
.bind(host, port)
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
serverChannel = future.channel();
plugin.getPluginLogger().info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
error.accept(null);
} else {
SocketAddress socketAddress = future.channel().localAddress();
if (socketAddress == null) {
socketAddress = new InetSocketAddress(host, port);
}
plugin.getPluginLogger().error("Votifier was not able to bind to {}", socketAddress, future.cause());
error.accept(future.cause());
}
});
}
public Bootstrap client() {
return new Bootstrap()
.channel(NioSocketChannel.class)
.group(eventLoopGroup);
}
public void shutdown() {
if (serverChannel != null) {
try {
serverChannel.close().syncUninterruptibly();
} catch (Exception e) {
plugin.getPluginLogger().error("Unable to shutdown server channel", e);
}
}
eventLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
@@ -7,6 +7,7 @@ public class VotifierSession {
public static final AttributeKey<VotifierSession> KEY = AttributeKey.valueOf("votifier_session");
private ProtocolVersion version = ProtocolVersion.UNKNOWN;
private final String challenge;
private boolean hasCompletedVote = false;
public VotifierSession() {
challenge = TokenUtil.newToken();
@@ -27,6 +28,17 @@ public class VotifierSession {
return challenge;
}
public void completeVote() {
if (hasCompletedVote)
throw new IllegalStateException("Protocol completed vote twice!");
hasCompletedVote = true;
}
public boolean hasCompletedVote() {
return hasCompletedVote;
}
public enum ProtocolVersion {
UNKNOWN,
ONE,
@@ -1,12 +1,13 @@
package com.vexsoftware.votifier.net.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.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.json.JSONObject;
public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
private final VoteHandler handler;
@@ -19,29 +20,31 @@ public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
protected void channelRead0(ChannelHandlerContext ctx, final Vote vote) throws Exception {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
handler.onVoteReceived(vote, session.getVersion());
handler.onVoteReceived(ctx.channel(), vote, session.getVersion());
session.completeVote();
if (session.getVersion() == VotifierSession.ProtocolVersion.ONE) {
ctx.close();
} else {
JSONObject object = new JSONObject();
object.put("status", "ok");
ctx.writeAndFlush(object.toString() + "\r\n").addListener(ChannelFutureListener.CLOSE);
JsonObject object = new JsonObject();
object.addProperty("status", "ok");
ctx.writeAndFlush(GsonInst.gson.toJson(object) + "\r\n").addListener(ChannelFutureListener.CLOSE);
}
}
@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(), cause);
handler.onError(ctx.channel(), session.hasCompletedVote(), cause);
if (session.getVersion() == VotifierSession.ProtocolVersion.TWO) {
JSONObject object = new JSONObject();
object.put("status", "error");
object.put("cause", cause.getClass().getSimpleName());
object.put("error", cause.getMessage());
ctx.writeAndFlush(object.toString() + "\r\n").addListener(ChannelFutureListener.CLOSE);
JsonObject object = new JsonObject();
object.addProperty("status", "error");
object.addProperty("cause", cause.getClass().getSimpleName());
object.addProperty("error", cause.getMessage());
ctx.writeAndFlush(GsonInst.gson.toJson(object) + "\r\n").addListener(ChannelFutureListener.CLOSE);
} else {
ctx.close();
}
@@ -1,6 +1,5 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -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,6 +1,6 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import io.netty.buffer.ByteBuf;
@@ -16,11 +16,15 @@ import java.util.List;
*/
public class VotifierProtocol1Decoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) throws Exception {
byte[] block = new byte[256];
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
if (buf.readableBytes() < 256) {
return;
}
byte[] block = new byte[buf.readableBytes()];
buf.getBytes(0, block);
// "Drain" the whole buffer
buf.readerIndex(buf.capacity());
buf.readerIndex(buf.readableBytes());
VotifierPlugin plugin = ctx.channel().attr(VotifierPlugin.KEY).get();
@@ -1,22 +1,19 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.util.GsonInst;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.MessageToMessageDecoder;
import org.json.JSONObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.security.*;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
@@ -28,39 +25,44 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
@Override
protected void decode(ChannelHandlerContext ctx, String s, List<Object> list) throws Exception {
JSONObject voteMessage = new JSONObject(s);
JsonObject voteMessage = GsonInst.gson.fromJson(s, JsonObject.class);
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
// Deserialize the payload.
JSONObject votePayload = new JSONObject(voteMessage.getString("payload"));
String payload = voteMessage.get("payload").getAsString();
JsonObject votePayload = GsonInst.gson.fromJson(payload, JsonObject.class);
// Verify challenge.
if (!votePayload.getString("challenge").equals(session.getChallenge())) {
if (!votePayload.get("challenge").getAsString().equals(session.getChallenge())) {
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.getString("serviceName"));
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.getString("serviceName") + "'");
throw new RuntimeException("Unknown service '" + votePayload.get("serviceName").getAsString() + "'");
}
}
// Verify signature.
String sigHash = voteMessage.getString("signature");
byte[] sigBytes = DatatypeConverter.parseBase64Binary(sigHash);
String sigHash = voteMessage.get("signature").getAsString();
byte[] sigBytes = Base64.getDecoder().decode(sigHash);
if (!hmacEqual(sigBytes, voteMessage.getString("payload").getBytes(StandardCharsets.UTF_8), key)) {
if (!hmacEqual(sigBytes, payload.getBytes(StandardCharsets.UTF_8), key)) {
throw new CorruptedFrameException("Signature is not valid (invalid token?)");
}
// Stopgap: verify the "uuid" field is valid, if provided.
if (votePayload.has("uuid")) {
UUID.fromString(votePayload.getString("uuid"));
UUID.fromString(votePayload.get("uuid").getAsString());
}
if (votePayload.get("username").getAsString().length() > 16) {
throw new CorruptedFrameException("Username too long");
}
// Create the vote.
@@ -75,16 +77,19 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
// This randomizes the byte order to make timing attacks more difficult.
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte[] messageCalc = mac.doFinal(message);
byte[] calculatedSig = mac.doFinal(message);
Mac mac2 = Mac.getInstance("HmacSHA256");
// Generate a random key for use in comparison
byte[] randomKey = new byte[32];
RANDOM.nextBytes(randomKey);
// Then generate two HMACs for the different signatures found
Mac mac2 = Mac.getInstance("HmacSHA256");
mac2.init(new SecretKeySpec(randomKey, "HmacSHA256"));
byte[] clientSig = mac2.doFinal(sig);
mac2.reset();
byte[] realSig = mac2.doFinal(messageCalc);
byte[] realSig = mac2.doFinal(calculatedSig);
return Arrays.equals(clientSig, realSig);
return MessageDigest.isEqual(clientSig, realSig);
}
}
@@ -26,15 +26,13 @@ public class VotifierProtocolDifferentiator extends ByteToMessageDecoder {
}
@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();
buf.retain(); // TODO: Is this needed?
if (readable < 2) {
// Some retarded voting sites (PMC?) seem to send empty buffers for no good reason.
// TODO: How can we handle this?
throw new CorruptedFrameException("Frame is too short");
return;
}
short readMagic = buf.readShort();
@@ -46,6 +44,8 @@ public class VotifierProtocolDifferentiator extends ByteToMessageDecoder {
if (readMagic == PROTOCOL_2_MAGIC) {
// 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));
@@ -53,17 +53,16 @@ public class VotifierProtocolDifferentiator extends ByteToMessageDecoder {
ctx.pipeline().addAfter("protocol2VoteDecoder", "protocol2StringEncoder", new StringEncoder(StandardCharsets.UTF_8));
ctx.pipeline().remove(this);
}
session.setVersion(VotifierSession.ProtocolVersion.TWO);
} else {
if (!allowv1) {
throw new CorruptedFrameException("This server only accepts well-formed Votifier v2 packets.");
}
// Probably Protocol v1 Vote Message
session.setVersion(VotifierSession.ProtocolVersion.ONE);
if (!testMode) {
ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder());
ctx.pipeline().remove(this);
}
session.setVersion(VotifierSession.ProtocolVersion.ONE);
}
}
}
@@ -18,9 +18,10 @@
package com.vexsoftware.votifier.net.protocol.v1crypto;
import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.KeyPair;
@@ -45,21 +46,17 @@ public class RSAIO {
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
// Store the public key.
// Store the public and private keys.
X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(
publicKey.getEncoded());
FileOutputStream out = new FileOutputStream(directory + "/public.key");
out.write(DatatypeConverter.printBase64Binary(publicSpec.getEncoded())
.getBytes());
out.close();
// Store the private key.
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(
privateKey.getEncoded());
out = new FileOutputStream(directory + "/private.key");
out.write(DatatypeConverter.printBase64Binary(privateSpec.getEncoded())
.getBytes());
out.close();
try (FileOutputStream publicOut = new FileOutputStream(directory + "/public.key");
FileOutputStream privateOut = new FileOutputStream(directory + "/private.key")) {
publicOut.write(Base64.getEncoder().encode(publicSpec.getEncoded()));
privateOut.write(Base64.getEncoder().encode(privateSpec.getEncoded()));
}
}
/**
@@ -74,14 +71,12 @@ public class RSAIO {
// Read the public key file.
File publicKeyFile = new File(directory + "/public.key");
byte[] encodedPublicKey = Files.readAllBytes(publicKeyFile.toPath());
encodedPublicKey = DatatypeConverter.parseBase64Binary(new String(
encodedPublicKey));
encodedPublicKey = Base64.getDecoder().decode(new String(encodedPublicKey, StandardCharsets.UTF_8));
// Read the private key file.
File privateKeyFile = new File(directory + "/private.key");
byte[] encodedPrivateKey = Files.readAllBytes(privateKeyFile.toPath());
encodedPrivateKey = DatatypeConverter.parseBase64Binary(new String(
encodedPrivateKey));
encodedPrivateKey = Base64.getDecoder().decode(new String(encodedPrivateKey, StandardCharsets.UTF_8));
// Instantiate and return the key pair.
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
@@ -0,0 +1,7 @@
package com.vexsoftware.votifier.platform;
public interface BackendServer {
String getName();
boolean sendPluginMessage(String channel, byte[] data);
}
@@ -0,0 +1,10 @@
package com.vexsoftware.votifier.platform;
import java.util.Collection;
import java.util.Optional;
public interface ProxyVotifierPlugin extends VotifierPlugin {
Collection<BackendServer> getAllBackendServers();
Optional<BackendServer> getServer(String name);
}
@@ -0,0 +1,26 @@
package com.vexsoftware.votifier.platform;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import java.security.Key;
import java.security.KeyPair;
import java.util.Map;
public interface VotifierPlugin extends VoteHandler {
AttributeKey<VotifierPlugin> KEY = AttributeKey.valueOf("votifier_plugin");
Map<String, Key> getTokens();
KeyPair getProtocolV1Key();
Logger getPluginLogger();
VotifierScheduler getScheduler();
default boolean isDebug() {
return false;
}
}
@@ -0,0 +1,53 @@
package com.vexsoftware.votifier.platform.scheduler;
import com.google.common.base.Preconditions;
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) {
this.service = Preconditions.checkNotNull(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 com.vexsoftware.votifier.platform.scheduler;
public interface ScheduledVotifierTask {
void cancel();
}
@@ -0,0 +1,15 @@
package com.vexsoftware.votifier.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);
}
@@ -0,0 +1,134 @@
package com.vexsoftware.votifier.support.forwarding;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.platform.ProxyVotifierPlugin;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.support.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.model.Vote;
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.function.Consumer;
import java.util.logging.Level;
public abstract class AbstractPluginMessagingForwardingSource implements ForwardingVoteSource {
public AbstractPluginMessagingForwardingSource(String channel, List<String> ignoredServers, ProxyVotifierPlugin plugin, VoteCache cache) {
this.channel = channel;
this.plugin = plugin;
this.cache = cache;
this.ignoredServers = ignoredServers;
}
protected AbstractPluginMessagingForwardingSource(String channel, ProxyVotifierPlugin plugin, VoteCache voteCache) {
this(channel, null, plugin, voteCache);
}
protected final ProxyVotifierPlugin plugin;
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 (BackendServer server : plugin.getAllBackendServers()) {
if (ignoredServers != null && ignoredServers.contains(server.getName())) continue;
if (!forwardSpecific(server, rawData)) attemptToAddToCache(v, server.getName());
}
}
protected boolean forwardSpecific(BackendServer connection, Vote vote) {
byte[] rawData = vote.serialize().toString().getBytes(StandardCharsets.UTF_8);
return connection.sendPluginMessage(channel, rawData);
}
private boolean forwardSpecific(BackendServer connection, byte[] data) {
return connection.sendPluginMessage(channel, data);
}
@Override
public void halt() {
if (cache instanceof FileVoteCache) {
try {
((FileVoteCache) cache).halt();
} catch (IOException e) {
plugin.getPluginLogger().error("Unable to save cached votes, votes will be lost.", e);
}
}
}
protected void onServerConnect(BackendServer server) {
if (cache == null) return;
final Collection<Vote> cachedVotes = cache.evict(server.getName());
dumpVotesToServer(cachedVotes, server, "server '" + server + "'", failedVotes -> {
for (Vote v : failedVotes)
cache.addToCache(v, server.getName());
});
}
protected void attemptToAddToCache(Vote v, String server) {
if (cache != null) {
cache.addToCache(v, server);
if (plugin.isDebug())
plugin.getPluginLogger().info("Added to forwarding cache: " + v + " -> " + server);
} else if (plugin.isDebug())
plugin.getPluginLogger().error("Could not immediately send vote to backend, vote lost! " + v + " -> " + server);
}
protected void attemptToAddToPlayerCache(Vote v, String player) {
if (cache != null) {
cache.addToCachePlayer(v, player);
if (plugin.isDebug())
plugin.getPluginLogger().info("Added to forwarding cache: " + v + " -> (player) " + player);
} else if (plugin.isDebug())
plugin.getPluginLogger().error("Could not immediately send vote to backend, vote lost! " + v + " -> (player) " + player);
}
// returns a collection of failed votes
private void dumpVotesToServer(Collection<Vote> cachedVotes, BackendServer target, String identifier, Consumer<List<Vote>> cb) {
List<Vote> failures = new ArrayList<>();
if (!cachedVotes.isEmpty()) {
plugin.getScheduler().delayedOnPool(() -> {
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 (plugin.isDebug()) {
plugin.getPluginLogger().info("Successfully evicted " + evicted + " votes to " + identifier + ".");
if (unsuccessfulEvictions > 0) {
plugin.getPluginLogger().info("Held " + unsuccessfulEvictions + " votes for " + identifier + ".");
}
}
cb.accept(failures);
}, 3, TimeUnit.SECONDS);
} else {
cb.accept(failures);
}
}
protected void handlePlayerSwitch(BackendServer server, String playerName) {
if (cache == null) return;
if (ignoredServers != null && ignoredServers.contains(server.getName())) return;
final Collection<Vote> cachedVotes = cache.evictPlayer(playerName);
dumpVotesToServer(cachedVotes, server, "player '" + playerName + "'", failedVotes -> {
for (Vote v : failedVotes)
cache.addToCachePlayer(v, playerName);
});
}
}
@@ -1,4 +1,4 @@
package com.vexsoftware.votifier.bungee.forwarding;
package com.vexsoftware.votifier.support.forwarding;
import com.vexsoftware.votifier.model.Vote;
@@ -0,0 +1,172 @@
package com.vexsoftware.votifier.support.forwarding.cache;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.util.GsonInst;
import org.slf4j.Logger;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class FileVoteCache extends MemoryVoteCache {
private final Logger l;
private final File cacheFile;
private final long voteTTL;
private final ScheduledVotifierTask saveTask;
public FileVoteCache(int initialMemorySize, final VotifierPlugin plugin, File cacheFile, long voteTTL) throws IOException {
super(initialMemorySize);
this.cacheFile = cacheFile;
this.voteTTL = voteTTL;
this.l = plugin.getPluginLogger();
load();
saveTask = plugin.getScheduler().repeatOnPool(() -> {
try {
save();
} catch (IOException e) {
l.error("Unable to save cached votes, votes will be lost if you restart.", e);
}
}, 3, 3, TimeUnit.MINUTES);
}
private static Set<String> keySet(JsonObject object) {
Set<String> set = new HashSet<>();
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
set.add(entry.getKey());
}
return set;
}
private void load() throws IOException {
// Load the cache from disk
JsonObject object;
try (BufferedReader reader = Files.newReader(cacheFile, StandardCharsets.UTF_8)) {
object = GsonInst.gson.fromJson(reader, JsonObject.class);
} catch (FileNotFoundException e) {
object = new JsonObject();
}
boolean resave = false;
// First, lets figure out if we are converting from a pre2.3.6 cache
if (!object.has("players") || !object.has("servers") || !object.has("version") || object.size() != 3) {
JsonObject oldObject = object;
object = new JsonObject();
object.add("servers", oldObject);
object.add("players", new JsonObject());
object.addProperty("version", 2);
resave = true;
}
if (object.get("version").getAsInt() != 2)
throw new IllegalStateException("Could not read cache file! Unknown version '" + object.get("version").getAsInt() + "' read.");
JsonObject players = object.getAsJsonObject("players");
JsonObject servers = object.getAsJsonObject("servers");
for (String player : keySet(players)) {
playerVoteCache.put(player, readVotes(players.getAsJsonArray(player)));
}
for (String server : keySet(servers)) {
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.error("Backup movement failed! Will not save.");
return;
}
l.warn("Saving new vote cache format to file - backup moved to " + replacementFile.getAbsolutePath());
save();
}
}
private Collection<Vote> readVotes(JsonArray voteArray) {
List<Vote> votes = new ArrayList<>(voteArray.size());
for (int i = 0; i < voteArray.size(); i++) {
JsonObject voteObject = voteArray.get(i).getAsJsonObject();
Vote v = new Vote(voteObject);
if (hasTimedOut(v))
l.warn("Purging out of date vote.", v);
else
votes.add(v);
}
return votes;
}
public void save() throws IOException {
cacheLock.lock();
JsonObject votesObject = new JsonObject();
votesObject.addProperty("version", 2);
try {
votesObject.add("players", serializeMap(playerVoteCache));
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.warn("Purging out of date vote.", vote);
voteItr.remove();
} else {
array.add(vote.serialize());
}
}
// if, during our iteration, we TTL invalidated all of the votes
if (entry.getValue().isEmpty())
entryItr.remove();
o.add(entry.getKey(), array);
}
return o;
}
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();
}
}
@@ -0,0 +1,90 @@
package com.vexsoftware.votifier.support.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,8 +1,9 @@
package com.vexsoftware.votifier.bungee.forwarding.cache;
package com.vexsoftware.votifier.support.forwarding.cache;
import com.vexsoftware.votifier.model.Vote;
import java.util.Collection;
import java.util.Collections;
/**
* Represents a method of caching votes for forwarding to a server that it was not previously capable of sending.
@@ -24,6 +25,16 @@ public interface VoteCache {
*/
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
*
@@ -31,4 +42,15 @@ public interface VoteCache {
* @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,17 +1,15 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy;
package com.vexsoftware.votifier.support.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.platform.VotifierPlugin;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierProtocol2Encoder;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierProtocol2HandshakeHandler;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierResponseHandler;
import com.vexsoftware.votifier.model.Vote;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.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;
@@ -23,20 +21,20 @@ import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.function.Supplier;
public class ProxyForwardingVoteSource implements ForwardingVoteSource {
private static final int MAX_RETRIES = 5;
private final NuVotifier plugin;
private final NioEventLoopGroup eventLoopGroup;
private final VotifierPlugin plugin;
private final Supplier<Bootstrap> nettyBootstrap;
private final List<BackendServer> backendServers;
private final VoteCache voteCache;
private static final StringDecoder STRING_DECODER = new StringDecoder(StandardCharsets.US_ASCII);
public ProxyForwardingVoteSource(NuVotifier plugin, NioEventLoopGroup eventLoopGroup, List<BackendServer> backendServers, VoteCache voteCache) {
public ProxyForwardingVoteSource(VotifierPlugin plugin, Supplier<Bootstrap> nettyBootstrap, List<BackendServer> backendServers, VoteCache voteCache) {
this.plugin = plugin;
this.eventLoopGroup = eventLoopGroup;
this.nettyBootstrap = nettyBootstrap;
this.backendServers = backendServers;
this.voteCache = voteCache;
}
@@ -54,21 +52,19 @@ public class ProxyForwardingVoteSource implements ForwardingVoteSource {
}
private void forwardVote(final BackendServer server, final Vote v, final int tries) {
new Bootstrap()
.channel(NioSocketChannel.class)
.group(eventLoopGroup)
nettyBootstrap.get()
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
protected void initChannel(NioSocketChannel channel) {
channel.pipeline().addLast(new DelimiterBasedFrameDecoder(256, true, Delimiters.lineDelimiter()));
channel.pipeline().addLast(new ReadTimeoutHandler(8, TimeUnit.SECONDS));
channel.pipeline().addLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS));
channel.pipeline().addLast(STRING_DECODER);
channel.pipeline().addLast(new VotifierProtocol2Encoder(server.key));
channel.pipeline().addLast(new VotifierProtocol2HandshakeHandler(v, new VotifierResponseHandler() {
@Override
public void onSuccess() {
if (plugin.isDebug()) {
plugin.getLogger().info("Successfully forwarded vote " + v + " to " + server.address + ".");
plugin.getPluginLogger().info("Successfully forwarded vote " + v + " to " + server.address + ".");
}
}
@@ -76,16 +72,13 @@ public class ProxyForwardingVoteSource implements ForwardingVoteSource {
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);
}
.addListener((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
handleFailure(server, v, future.cause(), tries);
}
});
}
@@ -106,18 +99,13 @@ public class ProxyForwardingVoteSource implements ForwardingVoteSource {
}
if (plugin.isDebug()) {
plugin.getLogger().log(Level.SEVERE, msg, cause);
plugin.getPluginLogger().error(msg, cause);
} else {
plugin.getLogger().log(Level.SEVERE, msg);
plugin.getPluginLogger().error(msg);
}
if (willRetry) {
plugin.getProxy().getScheduler().schedule(plugin, new Runnable() {
@Override
public void run() {
forwardVote(server, v, tries + 1);
}
}, nextDelay, TimeUnit.SECONDS);
plugin.getScheduler().delayedOnPool(() -> forwardVote(server, v, tries + 1), nextDelay, TimeUnit.SECONDS);
}
}
@@ -1,4 +1,4 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.vexsoftware.votifier.model.Vote;
@@ -1,15 +1,15 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.google.gson.JsonObject;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.json.JSONObject;
import javax.crypto.Mac;
import javax.xml.bind.DatatypeConverter;
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;
@@ -22,18 +22,18 @@ public class VotifierProtocol2Encoder extends MessageToByteEncoder<VoteRequest>
@Override
protected void encode(ChannelHandlerContext ctx, VoteRequest req, ByteBuf buf) throws Exception {
JSONObject object = new JSONObject();
JSONObject payloadObject = req.getVote().serialize();
payloadObject.put("challenge", req.getChallenge());
JsonObject object = new JsonObject();
JsonObject payloadObject = req.getVote().serialize();
payloadObject.addProperty("challenge", req.getChallenge());
String payload = payloadObject.toString();
object.put("payload", payload);
object.addProperty("payload", payload);
// Generate the MAC
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(payload.getBytes(StandardCharsets.UTF_8));
String computed = DatatypeConverter.printBase64Binary(mac.doFinal());
object.put("signature", computed);
String computed = Base64.getEncoder().encodeToString(mac.doFinal());
object.addProperty("signature", computed);
// JSON message is ready for encoding.
String finalMessage = object.toString();
@@ -1,5 +1,6 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@@ -8,21 +9,25 @@ import io.netty.handler.codec.CorruptedFrameException;
public class VotifierProtocol2HandshakeHandler extends SimpleChannelInboundHandler<String> {
private final Vote toSend;
private final VotifierResponseHandler responseHandler;
private final VotifierPlugin nuVotifier;
public VotifierProtocol2HandshakeHandler(Vote toSend, VotifierResponseHandler responseHandler) {
public VotifierProtocol2HandshakeHandler(Vote toSend, VotifierResponseHandler responseHandler, VotifierPlugin nuVotifier) {
this.toSend = toSend;
this.responseHandler = responseHandler;
this.nuVotifier = nuVotifier;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
protected void channelRead0(ChannelHandlerContext ctx, String s) {
String[] handshakeContents = s.split(" ");
if (handshakeContents.length != 3) {
throw new CorruptedFrameException("Handshake is not valid.");
}
VoteRequest request = new VoteRequest(handshakeContents[2], toSend);
System.out.println(request);
if (nuVotifier.isDebug()) {
nuVotifier.getPluginLogger().info("Sent request: " + request.toString());
}
ctx.writeAndFlush(request);
ctx.pipeline().addLast(new VotifierProtocol2ResponseHandler(responseHandler));
ctx.pipeline().remove(this);
@@ -30,7 +35,7 @@ public class VotifierProtocol2HandshakeHandler extends SimpleChannelInboundHandl
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
responseHandler.onFailure(cause);
ctx.close();
}
@@ -1,8 +1,9 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.util.GsonInst;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.json.JSONObject;
public class VotifierProtocol2ResponseHandler extends SimpleChannelInboundHandler<String> {
private final VotifierResponseHandler responseHandler;
@@ -12,19 +13,20 @@ public class VotifierProtocol2ResponseHandler extends SimpleChannelInboundHandle
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JSONObject object = new JSONObject(msg);
String status = object.getString("status");
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
JsonObject object = GsonInst.gson.fromJson(msg, JsonObject.class);
String status = object.get("status").getAsString();
if (status.equals("ok")) {
responseHandler.onSuccess();
} else {
responseHandler.onFailure(new Exception("Remote server error: " + object.getString("cause") + ": " + object.getString("error")));
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 {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
responseHandler.onFailure(cause);
ctx.close();
}
@@ -1,4 +1,4 @@
package com.vexsoftware.votifier.bungee.forwarding.proxy.client;
package com.vexsoftware.votifier.support.forwarding.proxy.client;
public interface VotifierResponseHandler {
void onSuccess();
@@ -0,0 +1,10 @@
package com.vexsoftware.votifier.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class GsonInst {
public static final Gson gson = new GsonBuilder().create();
}
@@ -0,0 +1,79 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.platform.scheduler.ScheduledExecutorServiceVotifierScheduler;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.KeyPair;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
public class StandaloneVotifierPlugin implements VotifierPlugin {
private final Map<String, Key> tokens;
private final VoteReceiver receiver;
private final KeyPair v1Key;
private final InetSocketAddress bind;
private final VotifierScheduler scheduler;
private VotifierServerBootstrap bootstrap;
public StandaloneVotifierPlugin(Map<String, Key> tokens, VoteReceiver receiver, KeyPair v1Key, InetSocketAddress bind) {
this.receiver = receiver;
this.bind = bind;
this.tokens = Collections.unmodifiableMap(new HashMap<>(tokens));
this.v1Key = v1Key;
this.scheduler = new ScheduledExecutorServiceVotifierScheduler(Executors.newScheduledThreadPool(1));
}
public void start() {
start(o -> {});
}
public void start(Consumer<Throwable> error) {
if (bootstrap != null) {
bootstrap.shutdown();
}
this.bootstrap = new VotifierServerBootstrap(bind.getHostString(), bind.getPort(), this, v1Key == null);
this.bootstrap.start(error);
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return v1Key;
}
@Override
public Logger getPluginLogger() {
return LoggerFactory.getLogger(StandaloneVotifierPlugin.class);
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
@Override
public void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
receiver.onVote(vote);
}
@Override
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
receiver.onException(throwable);
}
}
@@ -0,0 +1,8 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.model.Vote;
public interface VoteReceiver {
void onVote(Vote vote) throws Exception;
void onException(Throwable throwable);
}
@@ -0,0 +1,57 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.util.KeyCreator;
import java.io.File;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.KeyPair;
import java.security.interfaces.RSAKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class VotifierServerBuilder {
private final Map<String, Key> keyMap = new HashMap<>();
private KeyPair v1Key;
private InetSocketAddress bind;
private VoteReceiver receiver;
public VotifierServerBuilder addToken(String service, String token) {
Objects.requireNonNull(service, "service");
Objects.requireNonNull(token, "key");
keyMap.put(service, KeyCreator.createKeyFrom(token));
return this;
}
public VotifierServerBuilder v1Key(KeyPair v1Key) {
this.v1Key = Objects.requireNonNull(v1Key, "v1Key");
if (!(v1Key.getPrivate() instanceof RSAKey)) {
throw new IllegalArgumentException("Provided key is not an RSA key.");
}
return this;
}
public VotifierServerBuilder v1KeyFolder(File file) throws Exception {
this.v1Key = RSAIO.load(Objects.requireNonNull(file, "file"));
return this;
}
public VotifierServerBuilder bind(InetSocketAddress bind) {
this.bind = Objects.requireNonNull(bind, "bind");
return this;
}
public VotifierServerBuilder receiver(VoteReceiver receiver) {
this.receiver = Objects.requireNonNull(receiver, "receiver");
return this;
}
public StandaloneVotifierPlugin create() {
Objects.requireNonNull(bind, "need an address to bind to");
Objects.requireNonNull(receiver, "need a receiver for votes");
return new StandaloneVotifierPlugin(keyMap, receiver, v1Key, bind);
}
}
@@ -1,9 +1,13 @@
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.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.util.KeyCreator;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import javax.xml.bind.DatatypeConverter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -12,6 +16,7 @@ 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;
@@ -23,7 +28,7 @@ public class TestVotifierPlugin implements VotifierPlugin {
URL resourceUrl = TestVotifierPlugin.class.getResource(u);
Path resourcePath = Paths.get(resourceUrl.toURI());
return DatatypeConverter.parseBase64Binary(new String(Files.readAllBytes(resourcePath), StandardCharsets.UTF_8));
return Base64.getDecoder().decode(new String(Files.readAllBytes(resourcePath), StandardCharsets.UTF_8));
}
static {
@@ -71,8 +76,13 @@ public class TestVotifierPlugin implements VotifierPlugin {
}
@Override
public String getVersion() {
return "2.3";
public Logger getPluginLogger() {
return null;
}
@Override
public VotifierScheduler getScheduler() {
return null;
}
public void specificKeysOnly() {
@@ -84,4 +94,9 @@ public class TestVotifierPlugin implements VotifierPlugin {
keyMap.clear();
keyMap.put("default", KeyCreator.createKeyFrom("test"));
}
@Override
public void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
}
}
@@ -0,0 +1,31 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
public class VoteUtil {
public static byte[] encodePOJOv1(Vote vote) throws Exception {
return encodePOJOv1(vote, TestVotifierPlugin.getI().getProtocolV1Key().getPublic());
}
public static byte[] encodePOJOv1(Vote vote, PublicKey key) throws Exception {
List<String> ordered = new ArrayList<>();
ordered.add("VOTE");
ordered.add(vote.getServiceName());
ordered.add(vote.getUsername());
ordered.add(vote.getAddress());
ordered.add(vote.getTimeStamp());
StringBuilder builder = new StringBuilder();
for (String s : ordered) {
builder.append(s).append('\n'); // naive join needed!
}
return RSA.encrypt(builder.toString().getBytes(StandardCharsets.UTF_8), key);
}
}
@@ -1,6 +1,6 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
@@ -29,16 +29,12 @@ public class VotifierProtocol1DecoderTest {
@Test
public void testSuccessfulDecode() throws Exception {
// Encode a test vote.
String voteString = "VOTE\nTest\ntest\ntest\ntest\n";
// For reference, this is the same vote as a POJO:
Vote votePojo = new Vote("Test", "test", "test", "test");
// Send the vote
EmbeddedChannel channel = createChannel();
byte[] encrypted = RSA.encrypt(voteString.getBytes(StandardCharsets.UTF_8), TestVotifierPlugin.getI().getProtocolV1Key().getPublic());
byte[] encrypted = VoteUtil.encodePOJOv1(votePojo);
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
assertTrue(channel.writeInbound(encryptedByteBuf));
@@ -81,8 +77,7 @@ public class VotifierProtocol1DecoderTest {
// Send the bad vote
EmbeddedChannel channel = createChannel();
String voteString = "VOTE\nTest\ntest\ntest\ntest\n";
byte[] encrypted = RSA.encrypt(voteString.getBytes(StandardCharsets.UTF_8), badPublicKey);
byte[] encrypted = VoteUtil.encodePOJOv1(new Vote("Test", "test", "test", "test"), badPublicKey);
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
try {
@@ -1,8 +1,10 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.VotifierPlugin;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.util.GsonInst;
import com.vexsoftware.votifier.util.KeyCreator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.CorruptedFrameException;
@@ -11,9 +13,9 @@ import org.json.JSONObject;
import org.junit.Test;
import javax.crypto.Mac;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import static org.junit.Assert.*;
@@ -32,13 +34,14 @@ public class VotifierProtocol2DecoderTest {
EmbeddedChannel channel = createChannel();
JSONObject object = new JSONObject();
JSONObject payload = vote.serialize();
payload.put("challenge", SESSION.getChallenge());
object.put("payload", payload.toString());
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",
DatatypeConverter.printBase64Binary(mac.doFinal(payload.toString().getBytes(StandardCharsets.UTF_8))));
Base64.getEncoder().encodeToString(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
if (expectSuccess) {
assertTrue(channel.writeInbound(object.toString()));
@@ -59,15 +62,15 @@ public class VotifierProtocol2DecoderTest {
}
@Test(expected = DecoderException.class)
public void testFailureDecodeBadPacket() throws Exception {
public void testFailureDecodeBadPacket() {
// Create a well-formed request
EmbeddedChannel channel = createChannel();
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JSONObject payload = vote.serialize();
payload.put("challenge", SESSION.getChallenge());
object.put("payload", payload.toString());
JsonObject payload = vote.serialize();
payload.addProperty("challenge", SESSION.getChallenge());
object.put("payload", GsonInst.gson.toJson(payload));
// We "forget" the signature.
try {
@@ -84,13 +87,14 @@ public class VotifierProtocol2DecoderTest {
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JSONObject payload = vote.serialize();
JsonObject payload = vote.serialize();
String payloadEncoded = GsonInst.gson.toJson(payload);
// We "forget" the challenge.
object.put("payload", payload.toString());
object.put("payload", payloadEncoded);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(TestVotifierPlugin.getI().getTokens().get("default"));
object.put("signature",
DatatypeConverter.printBase64Binary(mac.doFinal(payload.toString().getBytes(StandardCharsets.UTF_8))));
Base64.getEncoder().encodeToString(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
try {
channel.writeInbound(object.toString());
@@ -106,14 +110,15 @@ public class VotifierProtocol2DecoderTest {
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JSONObject payload = vote.serialize();
JsonObject payload = vote.serialize();
// We provide the wrong challenge.
payload.put("challenge", "not a challenge for me");
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",
DatatypeConverter.printBase64Binary(mac.doFinal(payload.toString().getBytes(StandardCharsets.UTF_8))));
Base64.getEncoder().encode(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
try {
channel.writeInbound(object.toString());
@@ -1,5 +1,6 @@
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;
@@ -24,7 +25,6 @@ public class VotifierProtocolDifferentiatorTest {
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.ONE, session.getVersion());
test.release();
channel.close();
}
@@ -40,7 +40,6 @@ public class VotifierProtocolDifferentiatorTest {
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.TWO, session.getVersion());
test.release();
channel.close();
}
@@ -58,25 +57,9 @@ public class VotifierProtocolDifferentiatorTest {
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.ONE, session.getVersion());
test.release();
channel.close();
}
@Test(expected = DecoderException.class)
public void failOnSmallBufferTest() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, false));
ByteBuf buf = Unpooled.buffer(1);
buf.writeByte(0);
try {
channel.writeInbound(buf);
} finally {
buf.release();
channel.close();
}
}
@Test(expected = DecoderException.class)
public void failOnBadPacketTest() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, false));
@@ -89,8 +72,24 @@ public class VotifierProtocolDifferentiatorTest {
try {
channel.writeInbound(buf);
} finally {
buf.release();
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();
}
}
+80
View File
@@ -0,0 +1,80 @@
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:version"
...
}
...
```
# 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
@@ -0,0 +1,16 @@
# 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).
+12 -44
View File
@@ -1,19 +1,6 @@
# NuVotifier
This document outlines some of the technical reasons for a new 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.
## Why fork Votifier?
Votifier has lagged in activity, has severe security flaws in its protocol, and is vulnerable to denial of service attacks.
### 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.
### Why a new protocol?
# Why a new protocol?
The old Votifier protocol has three major flaws, which we feel are corrected in the new protocol.
@@ -21,39 +8,20 @@ The old Votifier protocol has three major flaws, which we feel are corrected in
* 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.
## Configuring NuVotifier
# Is NuVotifier a drop-in replacement for Votifier?
NuVotifier configures itself the first time it is run. If you have an existing Votifier configuration, NuVotifier will copy it over and use it.
Yes. NuVotifier is compatible with Votifier server lists and Votifier listeners, with some differences:
If you want to customize NuVotifier, simply the edit `./plugins/(Nu)Votifier/config.yml` file.
* The `Vote` object's setters are deprecated.
* Support for standalone vote listeners (`.class` files) has been removed, as handling `VotifierEvent` is more flexible.
An in-depth configuration guide is located in `usage.md` of this repository.
### Tokens
In `config.yml`, you will notice a `tokens` section. NuVotifier has a new protocol version that requires tokens. The `tokens` section can be used to easily grant or revoke access to a server list based on the keys it uses.
### RSA Keys
RSA keys for use with the old Votifier protocol are available under the `plugins/(Nu)Votifier/rsa` directory.
### Vote Forwarding
Starting from NuVotifier 2.2, there is a feature to forward votes from NuVotifier running on BungeeCord to your Bukkit servers without complicated setup.
You must enable the feature on both your Bukkit and BungeeCord servers. This feature is still in beta but is stable.
## Building NuVotifier
NuVotifier can be built by running `mvn clean package` from within the root directory of NuVotifier. A universal Bukkit/Spigot & BungeeCord plugin will be built to `universal/target/nuvotifier-universal-{version}.jar`
## Protocol Documentation
# Protocol Documentation
This documentation is for server lists that wish to add NuVotifier support.
There are two versions of the NuVotifier protocol.
### Handshake
## Handshake
A connection is made to the NuVotifier server by the server list, and immediately NuVotifier will send its version in the following packet:
@@ -61,7 +29,7 @@ A connection is made to the NuVotifier server by the server list, and immediatel
The challenge will not be present in version 1.x servers.
### Protocol v2
## 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.
@@ -72,9 +40,9 @@ The message is JSON-encoded containing the following data:
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) and [PHP](https://github.com/NuVotifier/votifier2-php). If you are stuck, these libraries may help you.
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)
## Protocol v1 (deprecated)
For better compatibility, NuVotifier supports the old Votifier protocol as well.
@@ -107,4 +75,4 @@ NuVotifier expects a 256 byte RSA encrypted block (the public key should be obta
</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.
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.
+238
View File
@@ -0,0 +1,238 @@
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 (such as BungeeCord, LilyPad, or Velocity).
## Single-Proxy BungeeCord/Velocity Networks
When running a single-proxy network, NuVotifier is able to operate as both
a vote receiver (emitting events for proxy 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 proxy server. The proxy's NuVotifier server will act as the
public facing Votifier instance, where as the Minecraft servers will instead
receive votes forwarded from the proxy's NuVotifier.
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 is
recommended if you have just one proxy, however this may also work with
some exceptions in multi-proxy 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:votes`.
Since 1.13, Spigot has decided that channels must now contain a colon. While
this is a completely senseless change, it is easier to simply conform and change
the channel name. 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 plugin messaging!
Since we have all of that configured, we can now move to the proxy's NuVotifier
configuration. It is once again located under `plugins/NuVotifier/` directory
(`config.yml` for BungeeCord, `config.toml` for Velocity). 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 plugin messaging 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
its 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 plugin to perform the proper forwarding logic
(you will know if you need to do this).
In proxy based vote forwarding, one of your proxy 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 the the port number as we will need
it later to insert into the proxy 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 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 proxy 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 proxy 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.
# Reloading NuVotifier
For Bukkit and Bungee, you may reload NuVotifier through the `/nvreload` command
in your console only - this command will not work in-game by design. Make sure to
ensure that it completed correctly before assuming NuVotifier works!
# 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.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#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-all.zip
Vendored Executable
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
Vendored
+84
View File
@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-57
View File
@@ -1,57 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier</artifactId>
<packaging>pom</packaging>
<version>2.3.2</version>
<modules>
<module>common</module>
<module>bukkit</module>
<module>bungeecord</module>
<module>universal</module>
<module>sponge</module>
</modules>
<name>NuVotifier</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</repository>
<repository>
<id>bukkit</id>
<url>http://repo.bukkit.org/content/groups/public/</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>minimumio-repo</id>
<url>http://repo.minimum.io/content/repositories/releases/</url>
</repository>
<snapshotRepository>
<id>minimumio-repo-snapshots</id>
<url>http://repo.minimum.io/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<!-- Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
+52
View File
@@ -0,0 +1,52 @@
// 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
}
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
rootProject.name = 'nuvotifier'
include 'common'
findProject(':common')?.name = 'nuvotifier-common'
include 'bukkit'
findProject(':bukkit')?.name = 'nuvotifier-bukkit'
include 'bungeecord'
findProject(':bungeecord')?.name = 'nuvotifier-bungeecord'
include 'sponge'
findProject(':sponge')?.name = 'nuvotifier-sponge'
include 'universal'
findProject(':universal')?.name = 'nuvotifier-universal'
include 'velocity'
findProject(':velocity')?.name = 'nuvotifier-velocity'
+4
View File
@@ -0,0 +1,4 @@
dependencies {
compileOnly group: "org.spongepowered", name: "spongeapi", version: "7.0.0-SNAPSHOT"
compile project(":nuvotifier-common")
}
-57
View File
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nuvotifier</artifactId>
<groupId>com.vexsoftware</groupId>
<version>2.3.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nuvotifier-sponge</artifactId>
<repositories>
<repository>
<id>sponge-maven-repo</id>
<name>Sponge maven repo</name>
<url>http://repo.spongepowered.org/maven</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-common</artifactId>
<version>${project.parent.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>spongeapi</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,58 @@
package com.vexsoftware.votifier.sponge;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.scheduler.Task;
import java.util.concurrent.TimeUnit;
class SpongeScheduler implements VotifierScheduler {
private final VotifierPlugin plugin;
SpongeScheduler(VotifierPlugin plugin) {
this.plugin = plugin;
}
private Task.Builder taskBuilder(Runnable runnable) {
return Sponge.getScheduler().createTaskBuilder().execute(runnable);
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return new TaskWrapper(taskBuilder(runnable).submit(plugin));
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new TaskWrapper(taskBuilder(runnable).async().submit(plugin));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).submit(plugin));
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).async().submit(plugin));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).interval(repeat, unit).submit(plugin));
}
private static class TaskWrapper implements ScheduledVotifierTask {
private final Task task;
private TaskWrapper(Task task) {
this.task = task;
}
@Override
public void cancel() {
task.cancel();
}
}
}
@@ -1,151 +1,59 @@
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.VotifierServerBootstrap;
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.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.sponge.config.ConfigLoader;
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.cause.Cause;
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.2")
public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.VotifierPlugin, ForwardedVoteListener {
private Logger logger;
@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.platform.VotifierPlugin, ForwardedVoteListener {
@Inject
public Logger logger;
@Inject
@ConfigDir(sharedRoot = false)
private Path baseDirectory;
public File configDir;
@Inject
public VotifierPlugin(Logger logger) {
this.logger = logger;
}
private VotifierScheduler scheduler;
@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;
}
}
this.scheduler = new SpongeScheduler(this);
// 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;
}
ConfigLoader.loadConfig(this);
/*
* Create RSA directory and keys if it does not exist; otherwise, read
* keys.
*/
File rsaDirectory = new File(configDir, "rsa");
try {
if (!rsaDirectory.exists()) {
rsaDirectory.mkdir();
@@ -160,47 +68,22 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
return;
}
debug = node.getNode("debug").getBoolean(false);
debug = ConfigLoader.getSpongeConfig().debug;
boolean setUpPort = node.getNode("enableExternal").getBoolean(true); //Always default to running the external port
// Load Votifier tokens.
ConfigLoader.getSpongeConfig().tokens.forEach((s, s2) -> {
tokens.put(s, KeyCreator.createKeyFrom(s2));
logger.info("Loaded token for website: " + s);
});
if (setUpPort) {
// Load Votifier tokens.
ConfigurationNode tokenSection = node.getNode("tokens");
// Initialize the receiver.
final String host = ConfigLoader.getSpongeConfig().host;
final int port = ConfigLoader.getSpongeConfig().port;
if (debug)
logger.info("DEBUG mode enabled!");
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!");
final boolean disablev1 = node.getNode("disable-v1-protocol").getBoolean(false);
if (port >= 0) {
final boolean disablev1 = ConfigLoader.getSpongeConfig().disableV1Protocol;
if (disablev1) {
logger.info("------------------------------------------------------------------------------");
logger.info("Votifier protocol v1 parsing has been disabled. Most voting websites do not");
@@ -208,48 +91,22 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
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.");
} 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());
}
}
});
this.bootstrap = new VotifierServerBootstrap(host, port, this, disablev1);
this.bootstrap.start(err -> {});
} else {
logger.info("You have enableExternal set to false in your config.yml. NuVotifier will NOT listen to votes coming in from an external voting list.");
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 (ConfigLoader.getSpongeConfig().forwarding != null) {
String method = ConfigLoader.getSpongeConfig().forwarding.method.toLowerCase(); //Default to lower case for case-insensitive searches
if ("none".equals(method)) {
getLogger().info("Method none selected for vote forwarding: Votes will not be received from a forwarder.");
} else if ("pluginmessaging".equals(method)) {
String channel = forwardingConfig.getNode("pluginMessaging").getNode("channel").getString("NuVotifier");
String channel = ConfigLoader.getSpongeConfig().forwarding.pluginMessaging.channel;
try {
forwardingMethod = new SpongePluginMessagingForwardingSink(this, channel, this);
getLogger().info("Receiving votes over PluginMessaging channel '" + channel + "'.");
@@ -264,11 +121,11 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
@Listener
public void onServerStop(GameStoppingServerEvent event) {
if (serverGroup != null) {
if (serverChannel != null)
serverChannel.close();
serverGroup.shutdownGracefully();
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null)
forwardingMethod.halt();
@@ -285,14 +142,9 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
private String version;
/**
* The server channel.
* The server bootstrap.
*/
private Channel serverChannel;
/**
* The event group handling the channel.
*/
private NioEventLoopGroup serverGroup;
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
@@ -315,13 +167,14 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
logger.error("Votifier did not initialize properly!");
}
/**
* Gets the version.
*
* @return The version
*/
public String getVersion() {
return version;
@Override
public Logger getPluginLogger() {
return logger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
@@ -338,32 +191,37 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
return keyPair;
}
public File getConfigDir() {
return configDir;
}
@Override
public void onVoteReceived(final Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
logger.info("Got a protocol v1 vote record -> " + vote);
logger.info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
logger.info("Got a protocol v2 vote record -> " + vote);
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, Cause.of(VotifierPlugin.this));
Sponge.getEventManager().post(event);
}
.execute(() -> {
VotifierEvent event = new VotifierEvent(vote, Sponge.getCauseStackManager().getCurrentCause());
Sponge.getEventManager().post(event);
})
.async()
.submit(this);
}
@Override
public void onError(Channel channel, Throwable throwable) {
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
logger.error("Unable to process vote from " + channel.remoteAddress(), throwable);
} else {
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());
}
}
@@ -374,14 +232,10 @@ public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.Vot
logger.info("Got a forwarded vote -> " + v);
}
Sponge.getScheduler().createTaskBuilder()
.execute(new Runnable() {
@Override
public void run() {
VotifierEvent event = new VotifierEvent(v, Cause.of(VotifierPlugin.this));
Sponge.getEventManager().post(event);
}
.execute(() -> {
VotifierEvent event = new VotifierEvent(v, Sponge.getCauseStackManager().getCurrentCause());
Sponge.getEventManager().post(event);
})
.async()
.submit(this);
}
}
@@ -0,0 +1,39 @@
package com.vexsoftware.votifier.sponge.config;
import com.google.common.reflect.TypeToken;
import com.vexsoftware.votifier.sponge.VotifierPlugin;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import java.io.File;
public class ConfigLoader {
private static VotifierPlugin plugin;
private static SpongeConfig spongeConfig;
public static void loadConfig(VotifierPlugin pl) {
plugin = pl;
if (!plugin.getConfigDir().exists()) {
plugin.getConfigDir().mkdirs();
}
try {
File file = new File(plugin.getConfigDir(), "config.yml");
if (!file.exists()) {
file.createNewFile();
}
ConfigurationLoader loader = YAMLConfigurationLoader.builder().setFile(file).build();
ConfigurationNode config = loader.load(ConfigurationOptions.defaults().setShouldCopyDefaults(true));
spongeConfig = config.getValue(TypeToken.of(SpongeConfig.class), new SpongeConfig());
loader.save(config);
} catch (Exception e) {
plugin.getLogger().error("Could not load config.", e);
}
}
public static SpongeConfig getSpongeConfig() {
return spongeConfig;
}
}
@@ -0,0 +1,55 @@
package com.vexsoftware.votifier.sponge.config;
import com.vexsoftware.votifier.util.TokenUtil;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
import org.spongepowered.api.Sponge;
import java.util.Collections;
import java.util.Map;
@ConfigSerializable
public class SpongeConfig {
@Setting(comment = "The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)\n" +
"This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.")
public String host = Sponge.getServer().getBoundAddress().isPresent() ? Sponge.getServer().getBoundAddress().get().getAddress().getHostAddress() : "0.0.0.0";
@Setting(comment = "Port to listen for new votes on")
public int port = 8192;
@Setting(comment = "Whether or not to print debug messages. In a production system, this should be set to false.\n" +
"This is useful when initially setting up NuVotifier to ensure votes are being delivered.")
public boolean debug = false;
@Setting( value = "disable-v1-protocol", comment = "Setting this option to true will disable handling of Protocol v1 packets. While the old protocol is not secure, this\n" +
"option is currently not recommended as most voting sites only support the old protocol at present. However, if you are\n" +
"using NuVotifier's proxy forwarding mechanism, enabling this option will increase your server's security.")
public boolean disableV1Protocol = false;
@Setting(comment = "All tokens, labeled by the serviceName of each server list.\n" +
"Default token for all server lists, if another isn't supplied.")
public Map<String, String> tokens = Collections.singletonMap("default", TokenUtil.newToken());
@Setting(comment = "Configuration section for all vote forwarding to NuVotifier")
public Forwarding forwarding = new Forwarding();
@ConfigSerializable
public static class Forwarding {
@Setting(comment = "Sets whether to set up a remote method for fowarding. Supported methods:\n" +
"- none - Does not set up a forwarding method.\n" +
"- pluginMessaging - Sets up plugin messaging")
public String method = "none";
@Setting
public PluginMessaging pluginMessaging = new PluginMessaging();
@ConfigSerializable
public static class PluginMessaging {
@Setting
public String channel = "NuVotifier:votes";
}
}
}
@@ -21,4 +21,8 @@ public class VotifierEvent extends AbstractEvent {
public Cause getCause() {
return cause;
}
public Vote getVote() {
return vote;
}
}
@@ -1,8 +1,9 @@
package com.vexsoftware.votifier.sponge.forwarding;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.sponge.VotifierPlugin;
import org.json.JSONObject;
import com.vexsoftware.votifier.util.GsonInst;
import org.spongepowered.api.Platform;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.network.ChannelBinding;
@@ -33,8 +34,9 @@ public class SpongePluginMessagingForwardingSink implements ForwardingVoteSink,
@Override
public void handlePayload(ChannelBuf channelBuf, RemoteConnection remoteConnection, Platform.Type type) {
try {
String message = new String(channelBuf.array(), StandardCharsets.UTF_8);
JSONObject jsonObject = new JSONObject(message);
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) {
@@ -9,10 +9,6 @@ port: 8192
# This is useful when initially setting up NuVotifier to ensure votes are being delivered.
debug: false
# Setting this value to false will turn off the votifier port listening for external votes. This is beneficial if the server
# is only listening for votes coming from NuVotifier running on your BungeeCord.
enableExternal: true
# 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.
@@ -30,4 +26,4 @@ forwarding:
# - pluginMessaging - Sets up plugin messaging
method: none
pluginMessaging:
channel: NuVotifier
channel: nuvotifier:votes
+37
View File
@@ -0,0 +1,37 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin: 'com.github.johnrengelman.shadow'
dependencies {
compile project(":nuvotifier-bukkit")
compile project(":nuvotifier-bungeecord")
compile project(":nuvotifier-sponge")
compile project(":nuvotifier-velocity")
}
shadowJar {
baseName = 'nuvotifier'
classifier = null
relocate "io.netty", "com.vexsoftware.votifier.netty"
relocate "org.json", "com.vexsoftware.votifier.json"
relocate "com.google.code", "com.vexsoftware.votifier.google"
relocate "com.google.guava", "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
-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nuvotifier</artifactId>
<groupId>com.vexsoftware</groupId>
<version>2.3.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nuvotifier-universal</artifactId>
<name>nuvotifier</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<relocations>
<relocation>
<pattern>io.netty</pattern>
<shadedPattern>com.vexsoftware.votifier.netty</shadedPattern>
</relocation>
<relocation>
<pattern>org.json</pattern>
<shadedPattern>com.vexsoftware.votifier.json</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.io</pattern>
<shadedPattern>com.vexsoftware.votifier.commons.io</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-bukkit</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-bungeecord</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-sponge</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>
-151
View File
@@ -1,151 +0,0 @@
This document outlines how to use NuVotifier as a plugin on both Bukkit based servers, as well as BungeeCord/Bukkit based
networks.
# Installation
The NuVotifier Universal jar (the default jar that is distributed on spigotmc.org) can be installed on Bukkit, BungeeCord,
and Sponge servers as-is without modification.
The Universal JAR can be downloaded off our [SpigotMC resource page](https://www.spigotmc.org/resources/nuvotifier.13449/).
# Votifier v1 vs NuVotifier v2 Protocols
NuVotifier introduces a more secure and failure tolerant protocol for sending votes. In this document, this new protocol
will be referred to as v2. NuVotifier also supports the legacy Votifier protocol (using public key encryption). In this document,
the legacy protocol will be referred to v1. As of writing, most if not all vote websites still use v1.
# Configuration
## Enabling the NuVotifier Server port
NuVotifier allows for the server port to be turned off when NuVotifier is only being used to forward votes. This is only
an option on Bukkit based servers, since it would not make logical sense for the port to be turned off on Bungee servers.
If you are receiving votes from a server list directly on the server in question (votes are not being forwarded through plugin
messaging), or if votes are being forwarded through the `proxy` method, then `enableExternal` should be set to true. Only
if you are receiving forwarded votes from your bungee through the `pluginMessaging` forwarding method should `enableExternal`
be set to false.
## Port Configuration
One of NuVotifier's functions is acting as a vote server (receiver). The IP for NuVotifier to bind to is configured
by the `host` and `ip` configuration values within the config.yml files within the NuVotifier plugin folder.
Most times, you will want this `host` value to be set to `0.0.0.0` or your server's ip. `0.0.0.0` will bind to all
ip ports on your machine, while specifying a certain IP will only bind to that ip. If you don't know what to set this
to, `0.0.0.0` is a safe bet. By default, NuVotifier listens on port 8192. This is the same as the default Votifier port.
If you change this, you will have to remember this when setting up forwarding or server lists.
## Debug mode
When first setting up NuVotifier, you will probably want debug mode to be enabled. This prints extra information to the
console, ensuring that votes are being processed as they should. If you are experiencing an error, or having issues with
votes being processed, you should first look to enable this. Debug mode can be turned on by setting `debug` in the config.yml
to true.
## Tokens vs Public Key
As explained earlier, NuVotifier supports 2 different protocols for receiving votes, v1 and v2. v1 uses the private and public
key pair located in `rsa/` in NuVotifier's plugin folder. When setting up voting websites, the public key, `rsa/public.key`
should be used. When setting up the `proxy` forwarding method, tokens should be used.
Making a new token is as easy as adding another entry into `tokens` with the serviceName as the key and whatever string you want
as the value. You can usually find the serviceName of a vote provider on their setup page if they use v2.
Unless a website specifies it, you should assume that the website is using v1, and you should enter your `rsa/public.key`.
## Forwarding
NuVotifier allows for various methods for forwarding the vote from an upstream Bungeecord instance. Currently, 2 methods
are supported: `proxy` and `pluginMessaging`. These are configured through the `forwarding` configuration section in your
config.yml files.
### Plugin Messaging Based Forwarding
Plugin Messaging based forwarding works by sending a forwarded vote over Minecraft's plugin messaging system. NuVotifier
is only capable of forwarding the vote if a player is on each backend server through the BungeeCord running NuVotifier.
Plugin Messaging can be enabled by setting `method` in `forwarding` to `pluginMessaging`. You may also set the plugin messaging
channel NuVotifier is to use by setting `channel` in the `pluginMessaging` section. This may be whatever you want, but it
must be the same on your BungeeCord as well as your backend Bukkit servers. If they are not the same, the backend will not
be able to receive the vote.
#### Filtering servers
If you only wish to send votes to some servers, but not all, there is a section within `pluginMessaging` called `excludedServers`.
By default, this section is commented out, and will never filter out any servers. If you wish to filter out servers, uncomment
this list and fill it with the names of servers you do not want to send votes to.
#### Plugin Messaging Vote Caching
In the event that the backend server does not have anyone on it from the BungeeCord instance, the vote will need to be cached
until a player joins and the vote may be sent. There are 3 options for caching, however one is only suitable for actual
production.
##### Memory Vote Caching
For votes to simply be cached in memory, you can set the `cache` setting of the `pluginMessaging` section of your BungeeCord's
NuVotifier's config.yml to `memory`. This stores votes in memory. The votes are lost when the BungeeCord server shuts down.
This is suitable for testing, but not for production system.
##### File Vote Caching
Votes can also be cached in a file located in the NuVotifier plugin folder. To enable this, you can set `cache` to file.
If you want to change the file location from the default, you can set this under the `name` config under the `file` section.
This caching method is suitable for most people.
##### No Caching
Caching can also be turned off. This is the default option. You can do this by setting `cache` to `none`. This is also not
recommended, and is only suitable for a certain number of setups.
### Proxy Based Forwarding
You can also forward votes using the proxy method. This will cause the BungeeCord to act as a voting websites, sending votes
to each server's NuVotifier port. To enable this method, you must set the `method` under `forwarding` to `proxy` in your
BungeeCord config.yml. Within the server's config.yml, `forwarding` should be set to `none`.
For each server you wish to forward to, you must add each to your BungeeCord config.yml under the `proxy` section of `forwarding`.
For each, you must specify the ip address (`address`), port (`port`) and token (`key`). Each backend Bukkit server should have
1 default token under the `tokens` section. This is the token that should be used as the token for the `key` in that server's
section within the `proxy` section. If the server is on the same server (same ip) as the BungeeCord server, you may
use the `127.0.0.1` or `localhost` address, setting both the `proxy` forwarding address and Bukkit config.yml address to it.
In order for vote proxying to work, the backend server __must__ have `enableExternal` set to true. If it is set to
false, the Bungee server will not be able to forward the vote.
## Default Configurations
The default configuration for Bukkit can be found [here](https://github.com/NuVotifier/NuVotifier/blob/master/bukkit/src/main/resources/bukkitConfig.yml),
for BungeeCord [here](https://github.com/NuVotifier/NuVotifier/blob/master/bungeecord/src/main/resources/bungeeConfig.yml),
and for Sponge [here](https://github.com/NuVotifier/NuVotifier/blob/master/sponge/src/main/resources/com/vexsoftware/votifier/sponge/spongeConfig.yml).
## Testing
There is a free resource for testing your NuVotifier setups at [ibj.io/v](https://ibj.io/v). If you are setting up your
network or server and want a server to get test votes on, you are more than welcome to use it.
## Basic Setups
### Single Server Quickstart
A single server setup should have `enableExternal` set to `true`, and `forwarding.method` set to `none`. The IP and port
should also be set to appropriate values. For server lists, you should use the public key found in `rsa/public.key`.
## Advanced Setups
### Multi BungeeCord Setups
Using NuVotifier with multiple BungeeCords is a more tricky setup. You can use one of your player-facing proxies for the
NuVotifier server, or use a separate one. You are not guaranteed to have players on all of your servers to allow votes to
be dispatched, so proxy forwarding (requiring `enableExternal` set to `true` on your Bukkit servers) is highly recommended.
## Contributing
If you wish to contribute code wise, feel free to submit a pull request!
## Client Libraries
NuVotifier has libraries available for [NodeJS](https://github.com/NuVotifier/votifier2-js) and [PHP](https://github.com/NuVotifier/votifier2-php).
Both are free to use by serverlists.
+4
View File
@@ -0,0 +1,4 @@
dependencies {
compileOnly group: "com.velocitypowered", name: "velocity-api", version: "1.0-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -0,0 +1,53 @@
package com.vexsoftware.votifier.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import java.util.Optional;
public final class OnlineForwardPluginMessagingForwardingSource extends AbstractPluginMessagingForwardingSource {
private final String fallbackServer;
private final VotifierPlugin plugin;
public OnlineForwardPluginMessagingForwardingSource(String channel, VotifierPlugin plugin, VoteCache cache, String fallbackServer) {
super(channel, plugin, cache);
this.fallbackServer = fallbackServer;
this.plugin = plugin;
plugin.getServer().getEventManager().register(plugin, this);
plugin.getServer().getChannelRegistrar().register((source, side, identifier, data) -> MessageHandler.ForwardStatus.HANDLED, VelocityUtil.getId(channel));
}
@Override
public void forward(Vote v) {
Optional<Player> p = plugin.getServer().getPlayer(v.getUsername());
Optional<ServerConnection> sc = p.flatMap(Player::getCurrentServer);
if (sc.isPresent()) {
if (forwardSpecific(new VelocityBackendServer(plugin.getServer(), sc.get().getServerInfo()), v)) {
return;
}
}
Optional<ServerInfo> fs = plugin.getServer().getServerInfo(fallbackServer);
// nowhere to fall back to, yet still not online. lets save this vote yet!
if (!fs.isPresent())
attemptToAddToPlayerCache(v, v.getUsername());
else if (!forwardSpecific(new VelocityBackendServer(plugin.getServer(), fs.get()), v))
attemptToAddToCache(v, fallbackServer);
}
@Subscribe
public void onServerConnected(final ServerConnectedEvent e) { //Attempt to resend any votes that were previously cached.
BackendServer server = new VelocityBackendServer(plugin.getServer(), e.getServer());
onServerConnect(server);
handlePlayerSwitch(server, e.getPlayer().getUsername());
}
}
@@ -0,0 +1,26 @@
package com.vexsoftware.votifier.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import java.util.List;
public class PluginMessagingForwardingSource extends AbstractPluginMessagingForwardingSource {
private final VotifierPlugin plugin;
public PluginMessagingForwardingSource(String channel, List<String> ignoredServers, VotifierPlugin plugin, VoteCache cache) {
super(channel, ignoredServers, plugin, cache);
this.plugin = plugin;
plugin.getServer().getEventManager().register(plugin, this);
plugin.getServer().getChannelRegistrar().register((source, side, identifier, data) -> MessageHandler.ForwardStatus.HANDLED, VelocityUtil.getId(channel));
}
@Subscribe
public void onServerConnected(final ServerConnectedEvent e) { //Attempt to resend any votes that were previously cached.
onServerConnect(new VelocityBackendServer(plugin.getServer(), e.getServer()));
}
}
@@ -0,0 +1,42 @@
package com.vexsoftware.votifier.velocity;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.vexsoftware.votifier.platform.BackendServer;
import java.util.Optional;
class VelocityBackendServer implements BackendServer {
private final ProxyServer server;
private final ServerInfo info;
VelocityBackendServer(ProxyServer server, ServerInfo info) {
this.server = server;
this.info = info;
}
@Override
public String getName() {
return info.getName();
}
@Override
public boolean sendPluginMessage(String channel, byte[] data) {
Optional<ServerConnection> connection = server.getAllPlayers().stream()
.map(p -> p.getCurrentServer().filter(s -> s.getServerInfo().equals(info)))
.filter(Optional::isPresent)
.findAny()
.flatMap(o -> o);
if (connection.isPresent()) {
connection.get().sendPluginMessage(VelocityUtil.getId(channel), data);
return true;
}
return false;
}
@Override
public String toString() {
return info.getName();
}
}
@@ -0,0 +1,61 @@
package com.vexsoftware.votifier.velocity;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.scheduler.ScheduledTask;
import com.velocitypowered.api.scheduler.Scheduler;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import java.util.concurrent.TimeUnit;
class VelocityScheduler implements VotifierScheduler {
private final ProxyServer server;
private final VotifierPlugin plugin;
public VelocityScheduler(ProxyServer server, VotifierPlugin plugin) {
this.server = server;
this.plugin = plugin;
}
private Scheduler.TaskBuilder builder(Runnable runnable) {
return server.getScheduler().buildTask(plugin, runnable);
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return onPool(runnable);
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new TaskWrapper(builder(runnable).schedule());
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return delayedOnPool(runnable, delay, unit);
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new TaskWrapper(builder(runnable).delay(delay, unit).schedule());
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new TaskWrapper(builder(runnable).delay(delay, unit).repeat(repeat, unit).schedule());
}
private static class TaskWrapper implements ScheduledVotifierTask {
private final ScheduledTask task;
private TaskWrapper(ScheduledTask task) {
this.task = task;
}
@Override
public void cancel() {
task.cancel();
}
}
}
@@ -0,0 +1,19 @@
package com.vexsoftware.votifier.velocity;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
public class VelocityUtil {
private VelocityUtil() {
throw new AssertionError();
}
public static ChannelIdentifier getId(String channel) {
if (channel.contains(":")) {
String[] split = channel.split(":");
return MinecraftChannelIdentifier.create(split[0], split[1]);
}
return new LegacyChannelIdentifier(channel);
}
}
@@ -0,0 +1,364 @@
package com.vexsoftware.votifier.velocity;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAKeygen;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.platform.ProxyVotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.support.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.MemoryVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.support.forwarding.proxy.ProxyForwardingVoteSource;
import com.vexsoftware.votifier.util.KeyCreator;
import com.vexsoftware.votifier.util.TokenUtil;
import com.vexsoftware.votifier.velocity.event.VotifierEvent;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyPair;
import java.util.*;
import java.util.stream.Collectors;
@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, ProxyVotifierPlugin {
@Inject
public Logger logger;
@Inject
@DataDirectory
public Path configDir;
@Inject
public ProxyServer server;
private VotifierScheduler scheduler;
@Subscribe
public void onServerStart(ProxyInitializeEvent event) {
this.scheduler = new VelocityScheduler(server, this);
// Load configuration.
Toml config;
try {
config = loadConfig();
} catch (IOException e) {
throw new RuntimeException("Unable to load configuration.", e);
}
/*
* Create RSA directory and keys if it does not exist; otherwise, read
* keys.
*/
File rsaDirectory = new File(configDir.toFile(), "rsa");
try {
if (!rsaDirectory.exists()) {
rsaDirectory.mkdir();
keyPair = RSAKeygen.generate(2048);
RSAIO.save(rsaDirectory, keyPair);
} else {
keyPair = RSAIO.load(rsaDirectory);
}
} catch (Exception ex) {
logger.error("Error creating or reading RSA tokens", ex);
gracefulExit();
return;
}
debug = config.getBoolean("debug");
// Load Votifier tokens.
config.getTable("tokens").toMap().forEach((service, key) -> {
if (key instanceof String) {
tokens.put(service, KeyCreator.createKeyFrom((String) key));
logger.info("Loaded token for website: " + service);
}
});
// Initialize the receiver.
final String host = config.getString("host");
final int port = Math.toIntExact(config.getLong("port"));
if (debug)
logger.info("DEBUG mode enabled!");
final boolean disablev1 = config.getBoolean("disable-v1-protocol", 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("------------------------------------------------------------------------------");
}
this.bootstrap = new VotifierServerBootstrap(host, port, this, disablev1);
this.bootstrap.start(err -> {});
Toml fwdCfg = config.getTable("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)) {
Toml pmCfg = fwdCfg.getTable("pluginMessaging");
String channel = pmCfg.getString("channel", "NuVotifier");
String cacheMethod = pmCfg.getString("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(server.getAllServers().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(
server.getAllServers().size(), this,
configDir.resolve(fwdCfg.getTable("file-cache").getString("name")).toFile(),
fwdCfg.getTable("file-cache").getLong("cacheTime", -1L));
} catch (IOException e) {
getLogger().error("Unload to load file cache. Votes will be lost!", e);
}
}
if (!pmCfg.getBoolean("onlySendToJoinedServer")) {
List<String> ignoredServers = pmCfg.getList("excludedServers");
try {
forwardingMethod = new PluginMessagingForwardingSource(channel, ignoredServers, this, voteCache);
getLogger().info("Forwarding votes over PluginMessaging channel '" + channel + "' for vote forwarding!");
} catch (RuntimeException e) {
getLogger().error("NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
} else {
try {
String fallbackServer = pmCfg.getString("joinedServerFallback", null);
if (fallbackServer != null && fallbackServer.isEmpty()) fallbackServer = null;
forwardingMethod = new OnlineForwardPluginMessagingForwardingSource(channel, this, voteCache, fallbackServer);
} catch (RuntimeException e) {
getLogger().error("NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
}
} else if ("proxy".equals(fwdMethod)) {
Toml serverSection = fwdCfg.getTable("proxy");
List<ProxyForwardingVoteSource.BackendServer> serverList = new ArrayList<>();
for (String s : serverSection.toMap().keySet()) {
Toml section = serverSection.getTable(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().error(
"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, Math.toIntExact(section.getLong("port"))),
token);
serverList.add(server);
}
}
forwardingMethod = new ProxyForwardingVoteSource(this, bootstrap::client, serverList, null);
getLogger().info("Forwarding votes from this NuVotifier instance to another NuVotifier server.");
} else {
getLogger().error("No vote forwarding method '" + fwdMethod + "' known. Defaulting to noop implementation.");
}
}
@Subscribe
public void onServerStop(ProxyShutdownEvent event) {
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null) {
forwardingMethod.halt();
forwardingMethod = null;
}
logger.info("Votifier disabled.");
}
public ProxyServer getServer() {
return server;
}
private Toml loadConfig() throws IOException {
if (!Files.exists(configDir)) {
Files.createDirectory(configDir);
}
Path configPath = configDir.resolve("config.toml");
try (Reader reader = Files.newBufferedReader(configPath, StandardCharsets.UTF_8)) {
return new Toml().read(reader);
} catch (NoSuchFileException e) {
// This is ok. Just copy the default and load that.
// First time run - do some initialization.
getLogger().info("Configuring Votifier for the first time...");
// Initialize the configuration file.
String cfgStr = new String(ByteStreams.toByteArray(VotifierPlugin.class.getResourceAsStream("/config.toml")), StandardCharsets.UTF_8);
String token = TokenUtil.newToken();
// TODO: Velocity doesn't expose any bind address, so use a temporary workaround
cfgStr = cfgStr.replace("%ip%", "0.0.0.0");
cfgStr = cfgStr.replace("%default_token%", token);
/*
* 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("------------------------------------------------------------------------------");
Files.copy(new ByteArrayInputStream(cfgStr.getBytes(StandardCharsets.UTF_8)), configPath);
return new Toml().read(cfgStr);
}
}
public Logger getLogger() {
return logger;
}
/**
* The current Votifier version.
*/
private String version;
/**
* The server bootstrap.
*/
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
*/
private KeyPair keyPair;
/**
* Debug mode flag
*/
private boolean debug;
/**
* Keys used for websites.
*/
private Map<String, Key> tokens = new HashMap<>();
/**
* Method used to forward votes to downstream servers
*/
private ForwardingVoteSource forwardingMethod;
private void gracefulExit() {
logger.error("Votifier did not initialize properly!");
}
@Override
public Logger getPluginLogger() {
return logger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
return debug;
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return keyPair;
}
@Override
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
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);
}
}
server.getEventManager().fireAndForget(new VotifierEvent(vote));
if (forwardingMethod != null) {
forwardingMethod.forward(vote);
}
}
@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 Collection<BackendServer> getAllBackendServers() {
return server.getAllServers().stream().map(s -> new VelocityBackendServer(server, s)).collect(Collectors.toList());
}
@Override
public Optional<BackendServer> getServer(String name) {
return server.getServerInfo(name).map(s -> new VelocityBackendServer(server, s));
}
}
@@ -0,0 +1,22 @@
package com.vexsoftware.votifier.velocity.event;
import com.vexsoftware.votifier.model.Vote;
public class VotifierEvent {
private final Vote vote;
public VotifierEvent(Vote vote) {
this.vote = vote;
}
public Vote getVote() {
return vote;
}
@Override
public String toString() {
return "VotifierEvent{" +
"vote=" + vote +
'}';
}
}
+70
View File
@@ -0,0 +1,70 @@
# The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)
# This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.
host = "%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.
# - proxy - Proxies votes to other NuVotifier servers from this server.
method = "none"
[forwarding.pluginMessaging]
channel = "nuvotifier:votes"
# Only send votes to certain servers. If a server is present on excludedServers, then votes will NOT be forwarded
# to them. If excludedServers is not present, votes will be sent to all servers.
# If onlySendToJoinedServer is true, 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"
[forwarding.file-cache]
# Options for file caching.
name = "cached-votes.json"
# days before a vote is considered 'dead' - removed from cache with a console warning
voteTTL = 10
[forwarding.proxy]
# Specify servers to proxy votes for.
[forwarding.proxy.Hub]
address = "127.0.0.1"
port = 8193
token = "MyKeyHere"
[forwarding.proxy.Hub2]
address = "127.0.0.1"
port = 8194
token = "MyKeyHere"