├── .github └── dependabot.yml ├── README.md └── VotifierPlus ├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs ├── org.eclipse.jdt.core.prefs └── org.eclipse.m2e.core.prefs ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── vexsoftware │ │ └── votifier │ │ ├── CheckUpdate.java │ │ ├── ForwardServer.java │ │ ├── VotifierPlus.java │ │ ├── bungee │ │ ├── BStatsMetricsBungee.java │ │ ├── Config.java │ │ ├── VotifierPlusBungee.java │ │ ├── VotifierPlusCommand.java │ │ └── events │ │ │ └── VotifierEvent.java │ │ ├── commands │ │ ├── CommandLoader.java │ │ ├── CommandVotifierPlus.java │ │ └── VotifierPlusTabCompleter.java │ │ ├── config │ │ └── Config.java │ │ ├── crypto │ │ ├── RSA.java │ │ ├── RSAIO.java │ │ ├── RSAKeygen.java │ │ └── TokenUtil.java │ │ ├── model │ │ ├── Vote.java │ │ └── VotifierEvent.java │ │ ├── net │ │ └── VoteReceiver.java │ │ └── velocity │ │ ├── Config.java │ │ ├── VotifierPlusVelocity.java │ │ ├── VotifierPlusVelocityCommand.java │ │ └── event │ │ └── VotifierEvent.java └── resources │ ├── bungee.yml │ ├── bungeeconfig.yml │ ├── config.yml │ ├── plugin.yml │ └── votifierplusversion.yml └── test └── java └── com └── bencodez └── votifierplus └── tests ├── RSATest.java ├── TokenUtilTest.java ├── VoteReceiverTest.java └── VoteTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/VotifierPlus/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VotifierPlus 2 | Fork of votifier 3 | -------------------------------------------------------------------------------- /VotifierPlus/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /VotifierPlus/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /target/ 3 | /doc/ 4 | /.idea/ 5 | /*.iml 6 | /.settings/ 7 | /.classpath 8 | /.apt_generated_tests/ 9 | -------------------------------------------------------------------------------- /VotifierPlus/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | VotifierPlus 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /VotifierPlus/.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/main/java=UTF-8 3 | encoding//src/main/resources=UTF-8 4 | encoding//src/test/java=UTF-8 5 | encoding/=UTF-8 6 | encoding/src=UTF-8 7 | -------------------------------------------------------------------------------- /VotifierPlus/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 5 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 6 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore 7 | org.eclipse.jdt.core.compiler.release=disabled 8 | org.eclipse.jdt.core.compiler.source=1.8 9 | -------------------------------------------------------------------------------- /VotifierPlus/.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles=prod 2 | eclipse.m2.autoUpdateProjects=true 3 | eclipse.preferences.version=1 4 | resolveWorkspaceProjects=true 5 | version=1 6 | -------------------------------------------------------------------------------- /VotifierPlus/pom.xml: -------------------------------------------------------------------------------- 1 | 5 | 4.0.0 6 | com.bencodez 7 | VotifierPlus 8 | 1.4.1 9 | jar 10 | VotifierPlus 11 | 12 | 1.8 13 | 1.8 14 | github 15 | UTF-8 16 | NOTSET 17 | 18 | 19 | 20 | internal.repo 21 | Temporary Staging Repository 22 | file://${project.build.directory}/mvn-repo 23 | 24 | 25 | 26 | 27 | 28 | src/main/resources 29 | true 30 | 31 | plugin.yml 32 | bungee.yml 33 | votifierplusversion.yml 34 | 35 | 36 | 37 | src/main/resources 38 | false 39 | 40 | 41 | src/main/java 42 | 43 | 44 | 45 | org.eclipse.m2e 46 | lifecycle-mapping 47 | 1.0.0 48 | 49 | 50 | 51 | 52 | 53 | org.projectlombok 54 | lombok-maven-plugin 55 | [1,) 56 | 57 | delombok 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-jar-plugin 74 | 3.4.2 75 | 76 | 77 | 78 | mojang 79 | 80 | 81 | ${project.name} 82 | 83 | 84 | 85 | com.coderplus.maven.plugins 86 | copy-rename-maven-plugin 87 | 1.0.1 88 | 89 | 90 | copy-file 91 | install 92 | 93 | copy 94 | 95 | 96 | 97 | ${project.basedir}/target/${project.name}.jar 98 | 99 | ${project.basedir}/target/${project.name}-${project.version}.jar 100 | 101 | 102 | 103 | 104 | 105 | org.projectlombok 106 | lombok-maven-plugin 107 | 1.18.20.0 108 | 109 | 110 | delombok 111 | deploy 112 | 113 | delombok 114 | 115 | 116 | false 117 | ${project.basedir}/target/delombok 118 | ${project.basedir}/src/main/java 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-shade-plugin 126 | 3.6.0 127 | 128 | false 129 | 130 | 131 | com.bencodez.simpleapi 132 | 133 | ${project.groupId}.votifierplus.simpleapi 134 | 135 | 138 | 139 | com.bencodez.advancedcore 140 | 141 | ${project.groupId}.${project.artifactId}.advancedcore 142 | 143 | 146 | 147 | net.pl3x.bukkit.chatapi 148 | 149 | ${project.groupId}.${project.artifactId} 150 | 151 | 152 | me.mrten.mysqlapi 153 | 154 | ${project.groupId}.${project.artifactId}.mysqlapi 155 | 156 | 157 | com.zaxxer.HikariCP 158 | 159 | ${project.groupId}.${project.artifactId}.HikariCP 160 | 161 | 162 | com.zaxxer.hikari 163 | 164 | ${project.groupId}.${project.artifactId}.hikari 165 | 166 | 167 | org.bstats 168 | 169 | ${project.groupId}.votingplugin.bstats 170 | 171 | 172 | 173 | 174 | 175 | package 176 | 177 | shade 178 | 179 | 180 | 181 | 184 | 185 | com.google.*:* 186 | 187 | 188 | false 189 | 190 | 191 | 192 | 193 | 194 | org.apache.maven.plugins 195 | maven-javadoc-plugin 196 | 3.11.2 197 | 198 | 199 | attach-javadocs 200 | 201 | jar 202 | 203 | deploy 204 | 205 | ${project.basedir}/target/delombok 206 | 207 | 208 | 209 | 210 | 211 | maven-resources-plugin 212 | 3.3.1 213 | 214 | 215 | copy-resources 216 | install 217 | 218 | copy-resources 219 | 220 | 221 | ${user.home}/Documents/Test_Server/plugins 222 | 223 | 224 | ${basedir}/target 225 | 226 | ${project.artifactId}.jar 227 | 228 | 229 | 230 | true 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | spigot-repo 240 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 241 | 242 | 243 | bencodez repo 244 | https://nexus.bencodez.com/repository/maven-public/ 245 | 246 | 247 | bungeecord-repo 248 | https://oss.sonatype.org/content/repositories/snapshots 249 | 250 | 251 | velocity 252 | https://nexus.velocitypowered.com/repository/maven-public/ 253 | 254 | 255 | 256 | 257 | org.spigotmc 258 | spigot-api 259 | 1.21.5-R0.1-SNAPSHOT 260 | provided 261 | 262 | 263 | org.projectlombok 264 | lombok 265 | 1.18.38 266 | provided 267 | 268 | 269 | net.md-5 270 | bungeecord-api 271 | 1.21-R0.2 272 | provided 273 | 274 | 275 | com.velocitypowered 276 | velocity-api 277 | 3.1.1 278 | provided 279 | 280 | 281 | org.bstats 282 | bstats-velocity 283 | 3.1.0 284 | compile 285 | 286 | 287 | com.bencodez 288 | simpleapi 289 | 0.0.6 290 | compile 291 | 292 | 293 | org.junit.jupiter 294 | junit-jupiter-engine 295 | 5.12.2 296 | test 297 | 298 | 299 | org.mockito 300 | mockito-core 301 | 5.18.0 302 | test 303 | 304 | 305 | 306 | 307 | prod 308 | 309 | true 310 | 311 | 312 | prod 313 | 314 | 315 | 316 | dev 317 | 318 | dev 319 | 320 | 321 | 322 | 323 | com.coderplus.maven.plugins 324 | copy-rename-maven-plugin 325 | 1.0.1 326 | 327 | 328 | copy-file 329 | install 330 | 331 | copy 332 | 333 | 334 | 335 | ${project.basedir}/target/${project.name}.jar 336 | 337 | ${project.basedir}/target/${project.name}-${build.number}.jar 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/CheckUpdate.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier; 2 | 3 | import com.bencodez.simpleapi.updater.Updater; 4 | 5 | // TODO: Auto-generated Javadoc 6 | /** 7 | * The Class CheckUpdate. 8 | */ 9 | public class CheckUpdate { 10 | 11 | /** The plugin. */ 12 | static VotifierPlus plugin; 13 | 14 | /** 15 | * Instantiates a new check update. 16 | * 17 | * @param plugin 18 | * the plugin 19 | */ 20 | public CheckUpdate(VotifierPlus plugin) { 21 | CheckUpdate.plugin = plugin; 22 | } 23 | 24 | /** 25 | * Check update. 26 | */ 27 | public void checkUpdate() { 28 | if (plugin.configFile.isDisableUpdateChecking()) { 29 | return; 30 | } 31 | plugin.setUpdater(new Updater(plugin, 74040, false)); 32 | final Updater.UpdateResult result = plugin.getUpdater().getResult(); 33 | switch (result) { 34 | case FAIL_SPIGOT: { 35 | plugin.getLogger().info("Failed to check for update for " + plugin.getName() + "!"); 36 | break; 37 | } 38 | case NO_UPDATE: { 39 | plugin.getLogger() 40 | .info(plugin.getName() + " is up to date! Version: " + plugin.getUpdater().getVersion()); 41 | break; 42 | } 43 | case UPDATE_AVAILABLE: { 44 | plugin.getLogger().info(plugin.getName() + " has an update available! Your Version: " 45 | + plugin.getDescription().getVersion() + " New Version: " + plugin.getUpdater().getVersion()); 46 | break; 47 | } 48 | default: { 49 | break; 50 | } 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/ForwardServer.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier; 2 | 3 | import lombok.Getter; 4 | 5 | public class ForwardServer { 6 | @Getter 7 | private String host; 8 | @Getter 9 | private int port; 10 | @Getter 11 | private String key; 12 | @Getter 13 | private boolean enabled; 14 | 15 | public ForwardServer(boolean enabled, String host, int port, String key) { 16 | this.enabled = enabled; 17 | this.host = host; 18 | this.port = port; 19 | this.key = key; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/VotifierPlus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | */ 18 | 19 | package com.vexsoftware.votifier; 20 | 21 | import java.io.File; 22 | import java.io.InputStreamReader; 23 | import java.io.Reader; 24 | import java.net.InetSocketAddress; 25 | import java.net.ServerSocket; 26 | import java.net.URL; 27 | import java.security.CodeSource; 28 | import java.security.Key; 29 | import java.security.KeyPair; 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.Map; 33 | import java.util.Set; 34 | import java.util.concurrent.Callable; 35 | import java.util.zip.ZipEntry; 36 | import java.util.zip.ZipInputStream; 37 | 38 | import org.bukkit.Bukkit; 39 | import org.bukkit.configuration.ConfigurationSection; 40 | import org.bukkit.configuration.file.YamlConfiguration; 41 | import org.bukkit.entity.Player; 42 | import org.bukkit.plugin.java.JavaPlugin; 43 | 44 | import com.bencodez.simpleapi.command.CommandHandler; 45 | import com.bencodez.simpleapi.command.TabCompleteHandle; 46 | import com.bencodez.simpleapi.command.TabCompleteHandler; 47 | import com.bencodez.simpleapi.debug.DebugLevel; 48 | import com.bencodez.simpleapi.metrics.BStatsMetrics; 49 | import com.bencodez.simpleapi.scheduler.BukkitScheduler; 50 | import com.bencodez.simpleapi.updater.Updater; 51 | import com.vexsoftware.votifier.commands.CommandLoader; 52 | import com.vexsoftware.votifier.commands.CommandVotifierPlus; 53 | import com.vexsoftware.votifier.commands.VotifierPlusTabCompleter; 54 | import com.vexsoftware.votifier.config.Config; 55 | import com.vexsoftware.votifier.crypto.RSAIO; 56 | import com.vexsoftware.votifier.crypto.RSAKeygen; 57 | import com.vexsoftware.votifier.crypto.TokenUtil; 58 | import com.vexsoftware.votifier.model.Vote; 59 | import com.vexsoftware.votifier.model.VotifierEvent; 60 | import com.vexsoftware.votifier.net.VoteReceiver; 61 | 62 | import lombok.Getter; 63 | import lombok.Setter; 64 | 65 | /** 66 | * The main Votifier plugin class. 67 | * 68 | * @author Blake Beaupain 69 | * @author Kramer Campbell 70 | */ 71 | public class VotifierPlus extends JavaPlugin { 72 | 73 | /** The Votifier instance. */ 74 | private static VotifierPlus instance; 75 | 76 | @Getter 77 | public Config configFile; 78 | 79 | @Getter 80 | @Setter 81 | private Updater updater; 82 | 83 | /** The vote receiver. */ 84 | private VoteReceiver voteReceiver; 85 | 86 | /** The RSA key pair. */ 87 | @Setter 88 | private KeyPair keyPair; 89 | 90 | @Getter 91 | private ArrayList commands = new ArrayList(); 92 | 93 | @Getter 94 | private String buildNumber = "NOTSET"; 95 | 96 | @Getter 97 | private String profile; 98 | 99 | @Getter 100 | private String time; 101 | 102 | @Getter 103 | private BukkitScheduler bukkitScheduler; 104 | 105 | private HashMap tokens = new HashMap(); 106 | 107 | private void loadTokens() { 108 | tokens.clear(); 109 | if (!configFile.getData().contains("tokens")) { 110 | configFile.setValue("tokens.default", TokenUtil.newToken()); 111 | } 112 | 113 | for (String key : configFile.getData().getConfigurationSection("tokens").getKeys(false)) { 114 | tokens.put(key, TokenUtil.createKeyFrom(configFile.getData().getString("tokens." + key))); 115 | } 116 | } 117 | 118 | @Override 119 | public void onEnable() { 120 | instance = this; 121 | bukkitScheduler = new BukkitScheduler(instance); 122 | 123 | configFile = new Config(this); 124 | configFile.setup(); 125 | 126 | if (configFile.isJustCreated()) { 127 | int openPort = 8192; 128 | try { 129 | ServerSocket s = new ServerSocket(); 130 | s.bind(new InetSocketAddress("0.0.0.0", 0)); 131 | openPort = s.getLocalPort(); 132 | s.close(); 133 | } catch (Exception e) { 134 | 135 | } 136 | try { 137 | // First time run - do some initialization. 138 | getLogger().info("Configuring Votifier for the first time..."); 139 | configFile.getData().set("port", openPort); 140 | configFile.saveData(); 141 | 142 | /* 143 | * Remind hosted server admins to be sure they have the right port number. 144 | */ 145 | getLogger().info("------------------------------------------------------------------------------"); 146 | getLogger().info("Assigning Votifier to listen on an open port " + openPort 147 | + ". If you are hosting server on a"); 148 | getLogger().info("shared server please check with your hosting provider to verify that this port"); 149 | getLogger().info("is available for your use. Chances are that your hosting provider will assign"); 150 | getLogger().info("a different port, which you need to specify in config.yml"); 151 | getLogger().info("------------------------------------------------------------------------------"); 152 | 153 | } catch (Exception ex) { 154 | getLogger().severe("Error creating configuration file"); 155 | debug(ex); 156 | } 157 | 158 | configFile.setValue("tokens.default", TokenUtil.newToken()); 159 | } 160 | configFile.loadValues(); 161 | loadTokens(); 162 | 163 | loadVersionFile(); 164 | 165 | getCommand("votifierplus").setExecutor(new CommandVotifierPlus(this)); 166 | getCommand("votifierplus").setTabCompleter(new VotifierPlusTabCompleter()); 167 | CommandLoader.getInstance().loadCommands(); 168 | loadTabComplete(); 169 | 170 | File rsaDirectory = new File(getDataFolder() + "/rsa"); 171 | 172 | /* 173 | * Create RSA directory and keys if it does not exist; otherwise, read keys. 174 | */ 175 | try { 176 | if (!rsaDirectory.exists()) { 177 | rsaDirectory.mkdir(); 178 | keyPair = RSAKeygen.generate(2048); 179 | RSAIO.save(rsaDirectory, keyPair); 180 | } else { 181 | keyPair = RSAIO.load(rsaDirectory); 182 | } 183 | } catch (Exception ex) { 184 | getLogger().severe("Error reading configuration file or RSA keys"); 185 | gracefulExit(); 186 | return; 187 | } 188 | 189 | loadVoteReceiver(); 190 | 191 | metrics(); 192 | 193 | getBukkitScheduler().runTaskLaterAsynchronously(this, new Runnable() { 194 | 195 | @Override 196 | public void run() { 197 | new CheckUpdate(instance).checkUpdate(); 198 | } 199 | }, 5); 200 | 201 | if (getProfile().contains("dev")) { 202 | getLogger().info( 203 | "Using dev build, this is not a stable build, use at your own risk. Build number: " + buildNumber); 204 | } 205 | } 206 | 207 | public void loadTabComplete() { 208 | TabCompleteHandler.getInstance() 209 | .addTabCompleteOption(new TabCompleteHandle("(Player)", new ArrayList()) { 210 | 211 | @Override 212 | public void reload() { 213 | ArrayList list = new ArrayList(); 214 | for (Player player : Bukkit.getOnlinePlayers()) { 215 | list.add(player.getName()); 216 | } 217 | setReplace(list); 218 | } 219 | 220 | @Override 221 | public void updateReplacements() { 222 | ArrayList list = new ArrayList(); 223 | for (Player player : Bukkit.getOnlinePlayers()) { 224 | list.add(player.getName()); 225 | } 226 | setReplace(list); 227 | } 228 | }.updateOnLoginLogout()); 229 | 230 | TabCompleteHandler.getInstance() 231 | .addTabCompleteOption(new TabCompleteHandle("(PlayerExact)", new ArrayList()) { 232 | 233 | @Override 234 | public void reload() { 235 | ArrayList list = new ArrayList(); 236 | for (Player player : Bukkit.getOnlinePlayers()) { 237 | list.add(player.getName()); 238 | } 239 | setReplace(list); 240 | } 241 | 242 | @Override 243 | public void updateReplacements() { 244 | ArrayList list = new ArrayList(); 245 | for (Player player : Bukkit.getOnlinePlayers()) { 246 | list.add(player.getName()); 247 | } 248 | setReplace(list); 249 | } 250 | }.updateOnLoginLogout()); 251 | 252 | ArrayList options = new ArrayList(); 253 | options.add("True"); 254 | options.add("False"); 255 | TabCompleteHandler.getInstance().addTabCompleteOption("(Boolean)", options); 256 | options = new ArrayList(); 257 | TabCompleteHandler.getInstance().addTabCompleteOption("(List)", options); 258 | TabCompleteHandler.getInstance().addTabCompleteOption("(String)", options); 259 | TabCompleteHandler.getInstance().addTabCompleteOption("(Text)", options); 260 | TabCompleteHandler.getInstance().addTabCompleteOption("(Number)", options); 261 | 262 | } 263 | 264 | private void loadVoteReceiver() { 265 | try { 266 | voteReceiver = new VoteReceiver(configFile.getHost(), configFile.getPort()) { 267 | 268 | @Override 269 | public void logWarning(String warn) { 270 | getLogger().warning(warn); 271 | } 272 | 273 | @Override 274 | public void logSevere(String msg) { 275 | getLogger().severe(msg); 276 | } 277 | 278 | @Override 279 | public void log(String msg) { 280 | getLogger().info(msg); 281 | } 282 | 283 | @Override 284 | public String getVersion() { 285 | return getDescription().getVersion(); 286 | } 287 | 288 | @Override 289 | public Set getServers() { 290 | return configFile.getServers(); 291 | } 292 | 293 | @Override 294 | public ForwardServer getServerData(String s) { 295 | ConfigurationSection d = configFile.getForwardingConfiguration(s); 296 | return new ForwardServer(d.getBoolean("Enabled"), d.getString("Host", ""), d.getInt("Port"), 297 | d.getString("Key", "")); 298 | } 299 | 300 | @Override 301 | public KeyPair getKeyPair() { 302 | return instance.getKeyPair(); 303 | } 304 | 305 | @Override 306 | public void debug(Exception e) { 307 | instance.debug(e); 308 | } 309 | 310 | @Override 311 | public void debug(String debug) { 312 | instance.debug(debug); 313 | } 314 | 315 | @Override 316 | public void callEvent(Vote vote) { 317 | getBukkitScheduler().executeOrScheduleSync(instance, new Runnable() { 318 | public void run() { 319 | Bukkit.getServer().getPluginManager().callEvent(new VotifierEvent(vote)); 320 | } 321 | }); 322 | } 323 | 324 | @Override 325 | public Map getTokens() { 326 | return tokens; 327 | } 328 | 329 | @Override 330 | public boolean isUseTokens() { 331 | return configFile.isTokenSupport(); 332 | } 333 | }; 334 | voteReceiver.start(); 335 | 336 | getLogger().info("Votifier enabled."); 337 | } catch (Exception ex) { 338 | gracefulExit(); 339 | return; 340 | } 341 | } 342 | 343 | public void debug(DebugLevel debugLevel, String debug) { 344 | if (debugLevel.equals(DebugLevel.EXTRA)) { 345 | debug = "ExtraDebug: " + debug; 346 | } else if (debugLevel.equals(DebugLevel.INFO)) { 347 | debug = "Debug: " + debug; 348 | } else if (debugLevel.equals(DebugLevel.DEV)) { 349 | debug = "Developer Debug: " + debug; 350 | } 351 | 352 | if (configFile.getDebug().isDebug(debugLevel)) { 353 | getLogger().info(debug); 354 | } 355 | } 356 | 357 | /** 358 | * Show exception in console if debug is on 359 | * 360 | * @param e Exception 361 | */ 362 | public void debug(Exception e) { 363 | if (getConfigFile().getDebug().isDebug()) { 364 | e.printStackTrace(); 365 | } 366 | } 367 | 368 | public void debug(String debug) { 369 | debug(DebugLevel.INFO, debug); 370 | } 371 | 372 | public void devDebug(String debug) { 373 | debug(DebugLevel.DEV, debug); 374 | } 375 | 376 | public void extraDebug(String debug) { 377 | debug(DebugLevel.EXTRA, debug); 378 | } 379 | 380 | @Override 381 | public void onDisable() { 382 | // Interrupt the vote receiver. 383 | if (voteReceiver != null) { 384 | voteReceiver.shutdown(); 385 | } 386 | getLogger().info("Votifier disabled."); 387 | } 388 | 389 | private void gracefulExit() { 390 | getLogger().severe("Votifier did not initialize properly!"); 391 | } 392 | 393 | /** 394 | * Gets the instance. 395 | * 396 | * @return The instance 397 | */ 398 | public static VotifierPlus getInstance() { 399 | return instance; 400 | } 401 | 402 | /** 403 | * Gets the vote receiver. 404 | * 405 | * @return The vote receiver 406 | */ 407 | public VoteReceiver getVoteReceiver() { 408 | return voteReceiver; 409 | } 410 | 411 | /** 412 | * Gets the keyPair. 413 | * 414 | * @return The keyPair 415 | */ 416 | public KeyPair getKeyPair() { 417 | return keyPair; 418 | } 419 | 420 | private void metrics() { 421 | BStatsMetrics metrics = new BStatsMetrics(this, 5807); 422 | metrics.addCustomChart(new BStatsMetrics.SimplePie("Forwarding", new Callable() { 423 | 424 | @Override 425 | public String call() throws Exception { 426 | int amount = 0; 427 | for (String server : configFile.getServers()) { 428 | if (configFile.getForwardingConfiguration(server).getBoolean("Enabled")) { 429 | amount++; 430 | } 431 | } 432 | return "" + amount; 433 | } 434 | })); 435 | if (!getBuildNumber().equals("NOTSET")) { 436 | metrics.addCustomChart(new BStatsMetrics.SimplePie("dev_build_number", new Callable() { 437 | 438 | @Override 439 | public String call() throws Exception { 440 | return "" + getBuildNumber(); 441 | } 442 | })); 443 | } 444 | } 445 | 446 | public void reload() { 447 | voteReceiver.shutdown(); 448 | configFile.reloadData(); 449 | loadTokens(); 450 | loadVoteReceiver(); 451 | } 452 | 453 | private YamlConfiguration getVersionFile() { 454 | try { 455 | CodeSource src = this.getClass().getProtectionDomain().getCodeSource(); 456 | if (src != null) { 457 | URL jar = src.getLocation(); 458 | ZipInputStream zip = null; 459 | zip = new ZipInputStream(jar.openStream()); 460 | while (true) { 461 | ZipEntry e = zip.getNextEntry(); 462 | if (e != null) { 463 | String name = e.getName(); 464 | if (name.equals("votifierplusversion.yml")) { 465 | Reader defConfigStream = new InputStreamReader(zip); 466 | if (defConfigStream != null) { 467 | YamlConfiguration defConfig = YamlConfiguration.loadConfiguration(defConfigStream); 468 | defConfigStream.close(); 469 | return defConfig; 470 | } 471 | } 472 | } 473 | } 474 | } 475 | } catch (Exception e) { 476 | e.printStackTrace(); 477 | } 478 | return null; 479 | } 480 | 481 | private void loadVersionFile() { 482 | YamlConfiguration conf = getVersionFile(); 483 | if (conf != null) { 484 | time = conf.getString("time", ""); 485 | profile = conf.getString("profile", ""); 486 | buildNumber = conf.getString("buildnumber", "NOTSET"); 487 | } 488 | } 489 | 490 | } 491 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/BStatsMetricsBungee.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.bungee; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.DataOutputStream; 7 | import java.io.File; 8 | import java.io.FileReader; 9 | import java.io.FileWriter; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.net.URL; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.UUID; 19 | import java.util.concurrent.Callable; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | import java.util.zip.GZIPOutputStream; 24 | 25 | import javax.net.ssl.HttpsURLConnection; 26 | 27 | import com.google.gson.JsonArray; 28 | import com.google.gson.JsonObject; 29 | import com.google.gson.JsonPrimitive; 30 | 31 | import net.md_5.bungee.api.plugin.Plugin; 32 | import net.md_5.bungee.config.Configuration; 33 | import net.md_5.bungee.config.ConfigurationProvider; 34 | import net.md_5.bungee.config.YamlConfiguration; 35 | 36 | /** 37 | * bStats collects some data for plugin authors. 38 | *

39 | * Check out https://bStats.org/ to learn more about bStats! 40 | */ 41 | public class BStatsMetricsBungee { 42 | 43 | /** 44 | * Represents a custom advanced bar chart. 45 | */ 46 | public static class AdvancedBarChart extends CustomChart { 47 | 48 | private final Callable> callable; 49 | 50 | /** 51 | * Class constructor. 52 | * 53 | * @param chartId The id of the chart. 54 | * @param callable The callable which is used to request the chart data. 55 | */ 56 | public AdvancedBarChart(String chartId, Callable> callable) { 57 | super(chartId); 58 | this.callable = callable; 59 | } 60 | 61 | @Override 62 | protected JsonObject getChartData() throws Exception { 63 | JsonObject data = new JsonObject(); 64 | JsonObject values = new JsonObject(); 65 | Map map = callable.call(); 66 | if (map == null || map.isEmpty()) { 67 | // Null = skip the chart 68 | return null; 69 | } 70 | boolean allSkipped = true; 71 | for (Map.Entry entry : map.entrySet()) { 72 | if (entry.getValue().length == 0) { 73 | continue; // Skip this invalid 74 | } 75 | allSkipped = false; 76 | JsonArray categoryValues = new JsonArray(); 77 | for (int categoryValue : entry.getValue()) { 78 | categoryValues.add(new JsonPrimitive(categoryValue)); 79 | } 80 | values.add(entry.getKey(), categoryValues); 81 | } 82 | if (allSkipped) { 83 | // Null = skip the chart 84 | return null; 85 | } 86 | data.add("values", values); 87 | return data; 88 | } 89 | 90 | } 91 | 92 | /** 93 | * Represents a custom advanced pie. 94 | */ 95 | public static class AdvancedPie extends CustomChart { 96 | 97 | private final Callable> callable; 98 | 99 | /** 100 | * Class constructor. 101 | * 102 | * @param chartId The id of the chart. 103 | * @param callable The callable which is used to request the chart data. 104 | */ 105 | public AdvancedPie(String chartId, Callable> callable) { 106 | super(chartId); 107 | this.callable = callable; 108 | } 109 | 110 | @Override 111 | protected JsonObject getChartData() throws Exception { 112 | JsonObject data = new JsonObject(); 113 | JsonObject values = new JsonObject(); 114 | Map map = callable.call(); 115 | if (map == null || map.isEmpty()) { 116 | // Null = skip the chart 117 | return null; 118 | } 119 | boolean allSkipped = true; 120 | for (Map.Entry entry : map.entrySet()) { 121 | if (entry.getValue() == 0) { 122 | continue; // Skip this invalid 123 | } 124 | allSkipped = false; 125 | values.addProperty(entry.getKey(), entry.getValue()); 126 | } 127 | if (allSkipped) { 128 | // Null = skip the chart 129 | return null; 130 | } 131 | data.add("values", values); 132 | return data; 133 | } 134 | } 135 | 136 | /** 137 | * Represents a custom chart. 138 | */ 139 | public static abstract class CustomChart { 140 | 141 | // The id of the chart 142 | private final String chartId; 143 | 144 | /** 145 | * Class constructor. 146 | * 147 | * @param chartId The id of the chart. 148 | */ 149 | CustomChart(String chartId) { 150 | if (chartId == null || chartId.isEmpty()) { 151 | throw new IllegalArgumentException("ChartId cannot be null or empty!"); 152 | } 153 | this.chartId = chartId; 154 | } 155 | 156 | protected abstract JsonObject getChartData() throws Exception; 157 | 158 | private JsonObject getRequestJsonObject(Logger logger, boolean logFailedRequests) { 159 | JsonObject chart = new JsonObject(); 160 | chart.addProperty("chartId", chartId); 161 | try { 162 | JsonObject data = getChartData(); 163 | if (data == null) { 164 | // If the data is null we don't send the chart. 165 | return null; 166 | } 167 | chart.add("data", data); 168 | } catch (Throwable t) { 169 | if (logFailedRequests) { 170 | logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); 171 | } 172 | return null; 173 | } 174 | return chart; 175 | } 176 | 177 | } 178 | 179 | /** 180 | * Represents a custom drilldown pie. 181 | */ 182 | public static class DrilldownPie extends CustomChart { 183 | 184 | private final Callable>> callable; 185 | 186 | /** 187 | * Class constructor. 188 | * 189 | * @param chartId The id of the chart. 190 | * @param callable The callable which is used to request the chart data. 191 | */ 192 | public DrilldownPie(String chartId, Callable>> callable) { 193 | super(chartId); 194 | this.callable = callable; 195 | } 196 | 197 | @Override 198 | public JsonObject getChartData() throws Exception { 199 | JsonObject data = new JsonObject(); 200 | JsonObject values = new JsonObject(); 201 | Map> map = callable.call(); 202 | if (map == null || map.isEmpty()) { 203 | // Null = skip the chart 204 | return null; 205 | } 206 | boolean reallyAllSkipped = true; 207 | for (Map.Entry> entryValues : map.entrySet()) { 208 | JsonObject value = new JsonObject(); 209 | boolean allSkipped = true; 210 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 211 | value.addProperty(valueEntry.getKey(), valueEntry.getValue()); 212 | allSkipped = false; 213 | } 214 | if (!allSkipped) { 215 | reallyAllSkipped = false; 216 | values.add(entryValues.getKey(), value); 217 | } 218 | } 219 | if (reallyAllSkipped) { 220 | // Null = skip the chart 221 | return null; 222 | } 223 | data.add("values", values); 224 | return data; 225 | } 226 | } 227 | 228 | /** 229 | * Represents a custom multi line chart. 230 | */ 231 | public static class MultiLineChart extends CustomChart { 232 | 233 | private final Callable> callable; 234 | 235 | /** 236 | * Class constructor. 237 | * 238 | * @param chartId The id of the chart. 239 | * @param callable The callable which is used to request the chart data. 240 | */ 241 | public MultiLineChart(String chartId, Callable> callable) { 242 | super(chartId); 243 | this.callable = callable; 244 | } 245 | 246 | @Override 247 | protected JsonObject getChartData() throws Exception { 248 | JsonObject data = new JsonObject(); 249 | JsonObject values = new JsonObject(); 250 | Map map = callable.call(); 251 | if (map == null || map.isEmpty()) { 252 | // Null = skip the chart 253 | return null; 254 | } 255 | boolean allSkipped = true; 256 | for (Map.Entry entry : map.entrySet()) { 257 | if (entry.getValue() == 0) { 258 | continue; // Skip this invalid 259 | } 260 | allSkipped = false; 261 | values.addProperty(entry.getKey(), entry.getValue()); 262 | } 263 | if (allSkipped) { 264 | // Null = skip the chart 265 | return null; 266 | } 267 | data.add("values", values); 268 | return data; 269 | } 270 | 271 | } 272 | 273 | /** 274 | * Represents a custom simple bar chart. 275 | */ 276 | public static class SimpleBarChart extends CustomChart { 277 | 278 | private final Callable> callable; 279 | 280 | /** 281 | * Class constructor. 282 | * 283 | * @param chartId The id of the chart. 284 | * @param callable The callable which is used to request the chart data. 285 | */ 286 | public SimpleBarChart(String chartId, Callable> callable) { 287 | super(chartId); 288 | this.callable = callable; 289 | } 290 | 291 | @Override 292 | protected JsonObject getChartData() throws Exception { 293 | JsonObject data = new JsonObject(); 294 | JsonObject values = new JsonObject(); 295 | Map map = callable.call(); 296 | if (map == null || map.isEmpty()) { 297 | // Null = skip the chart 298 | return null; 299 | } 300 | for (Map.Entry entry : map.entrySet()) { 301 | JsonArray categoryValues = new JsonArray(); 302 | categoryValues.add(new JsonPrimitive(entry.getValue())); 303 | values.add(entry.getKey(), categoryValues); 304 | } 305 | data.add("values", values); 306 | return data; 307 | } 308 | 309 | } 310 | 311 | /** 312 | * Represents a custom simple pie. 313 | */ 314 | public static class SimplePie extends CustomChart { 315 | 316 | private final Callable callable; 317 | 318 | /** 319 | * Class constructor. 320 | * 321 | * @param chartId The id of the chart. 322 | * @param callable The callable which is used to request the chart data. 323 | */ 324 | public SimplePie(String chartId, Callable callable) { 325 | super(chartId); 326 | this.callable = callable; 327 | } 328 | 329 | @Override 330 | protected JsonObject getChartData() throws Exception { 331 | JsonObject data = new JsonObject(); 332 | String value = callable.call(); 333 | if (value == null || value.isEmpty()) { 334 | // Null = skip the chart 335 | return null; 336 | } 337 | data.addProperty("value", value); 338 | return data; 339 | } 340 | } 341 | 342 | /** 343 | * Represents a custom single line chart. 344 | */ 345 | public static class SingleLineChart extends CustomChart { 346 | 347 | private final Callable callable; 348 | 349 | /** 350 | * Class constructor. 351 | * 352 | * @param chartId The id of the chart. 353 | * @param callable The callable which is used to request the chart data. 354 | */ 355 | public SingleLineChart(String chartId, Callable callable) { 356 | super(chartId); 357 | this.callable = callable; 358 | } 359 | 360 | @Override 361 | protected JsonObject getChartData() throws Exception { 362 | JsonObject data = new JsonObject(); 363 | int value = callable.call(); 364 | if (value == 0) { 365 | // Null = skip the chart 366 | return null; 367 | } 368 | data.addProperty("value", value); 369 | return data; 370 | } 371 | 372 | } 373 | 374 | // The version of this bStats class 375 | public static final int B_STATS_VERSION = 1; 376 | 377 | // A list with all known metrics class objects including this one 378 | private static final List knownMetricsInstances = new ArrayList<>(); 379 | 380 | // Should the response text be logged? 381 | private static boolean logResponseStatusText; 382 | 383 | // Should the sent data be logged? 384 | private static boolean logSentData; 385 | 386 | // The url to which the data is sent 387 | private static final String URL = "https://bStats.org/submitData/bungeecord"; 388 | 389 | static { 390 | // You can use the property to disable the check in your test environment 391 | if (System.getProperty("bstats.relocatecheck") == null 392 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 393 | // Maven's Relocate is clever and changes strings, too. So we have to use this 394 | // little "trick" ... :D 395 | final String defaultPackage = new String(new byte[] { 'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 396 | 'b', 'u', 'n', 'g', 'e', 'e', 'c', 'o', 'r', 'd' }); 397 | final String examplePackage = new String( 398 | new byte[] { 'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e' }); 399 | // We want to make sure nobody just copy & pastes the example and use the wrong 400 | // package names 401 | if (BStatsMetricsBungee.class.getPackage().getName().equals(defaultPackage) 402 | || BStatsMetricsBungee.class.getPackage().getName().equals(examplePackage)) { 403 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 404 | } 405 | } 406 | } 407 | 408 | /** 409 | * Gzips the given String. 410 | * 411 | * @param str The string to gzip. 412 | * @return The gzipped String. 413 | * @throws IOException If the compression failed. 414 | */ 415 | private static byte[] compress(final String str) throws IOException { 416 | if (str == null) { 417 | return null; 418 | } 419 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 420 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 421 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 422 | } 423 | return outputStream.toByteArray(); 424 | } 425 | 426 | /** 427 | * Links an other metrics class with this class. This method is called using 428 | * Reflection. 429 | * 430 | * @param metrics An object of the metrics class to link. 431 | */ 432 | public static void linkMetrics(Object metrics) { 433 | knownMetricsInstances.add(metrics); 434 | } 435 | 436 | /** 437 | * Sends the data to the bStats server. 438 | * 439 | * @param plugin Any plugin. It's just used to get a logger instance. 440 | * @param data The data to send. 441 | * @throws Exception If the request failed. 442 | */ 443 | private static void sendData(Plugin plugin, JsonObject data) throws Exception { 444 | if (data == null) { 445 | throw new IllegalArgumentException("Data cannot be null"); 446 | } 447 | if (logSentData) { 448 | plugin.getLogger().info("Sending data to bStats: " + data); 449 | } 450 | 451 | HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); 452 | 453 | // Compress the data to save bandwidth 454 | byte[] compressedData = compress(data.toString()); 455 | 456 | // Add headers 457 | connection.setRequestMethod("POST"); 458 | connection.addRequestProperty("Accept", "application/json"); 459 | connection.addRequestProperty("Connection", "close"); 460 | connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request 461 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 462 | connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format 463 | connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); 464 | 465 | // Send data 466 | connection.setDoOutput(true); 467 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 468 | outputStream.write(compressedData); 469 | } 470 | 471 | StringBuilder builder = new StringBuilder(); 472 | 473 | try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 474 | String line; 475 | while ((line = bufferedReader.readLine()) != null) { 476 | builder.append(line); 477 | } 478 | } 479 | 480 | if (logResponseStatusText) { 481 | plugin.getLogger().info("Sent data to bStats and received response: " + builder); 482 | } 483 | } 484 | 485 | // A list with all custom charts 486 | private final List charts = new ArrayList<>(); 487 | 488 | // Is bStats enabled on this server? 489 | private boolean enabled; 490 | 491 | // Should failed requests be logged? 492 | private boolean logFailedRequests = false; 493 | 494 | // The plugin 495 | private final Plugin plugin; 496 | 497 | // The plugin id 498 | private final int pluginId; 499 | 500 | // The uuid of the server 501 | private String serverUUID; 502 | 503 | /** 504 | * Class constructor. 505 | * 506 | * @param plugin The plugin which stats should be submitted. 507 | * @param pluginId The id of the plugin. It can be found at 508 | * What is my 509 | * plugin id? 510 | */ 511 | public BStatsMetricsBungee(Plugin plugin, int pluginId) { 512 | this.plugin = plugin; 513 | this.pluginId = pluginId; 514 | 515 | try { 516 | loadConfig(); 517 | } catch (IOException e) { 518 | // Failed to load configuration 519 | plugin.getLogger().log(Level.WARNING, "Failed to load bStats config!", e); 520 | return; 521 | } 522 | 523 | // We are not allowed to send data about this server :( 524 | if (!enabled) { 525 | return; 526 | } 527 | 528 | Class usedMetricsClass = getFirstBStatsClass(); 529 | if (usedMetricsClass == null) { 530 | // Failed to get first metrics class 531 | return; 532 | } 533 | if (usedMetricsClass == getClass()) { 534 | // We are the first! :) 535 | linkMetrics(this); 536 | startSubmitting(); 537 | } else { 538 | // We aren't the first so we link to the first metrics class 539 | try { 540 | usedMetricsClass.getMethod("linkMetrics", Object.class).invoke(null, this); 541 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 542 | if (logFailedRequests) { 543 | plugin.getLogger().log(Level.WARNING, 544 | "Failed to link to first metrics class " + usedMetricsClass.getName() + "!", e); 545 | } 546 | } 547 | } 548 | } 549 | 550 | /** 551 | * Adds a custom chart. 552 | * 553 | * @param chart The chart to add. 554 | */ 555 | public void addCustomChart(CustomChart chart) { 556 | if (chart == null) { 557 | plugin.getLogger().log(Level.WARNING, "Chart cannot be null"); 558 | } 559 | charts.add(chart); 560 | } 561 | 562 | /** 563 | * Gets the first bStat Metrics class. 564 | * 565 | * @return The first bStats metrics class. 566 | */ 567 | private Class getFirstBStatsClass() { 568 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 569 | bStatsFolder.mkdirs(); 570 | File tempFile = new File(bStatsFolder, "temp.txt"); 571 | 572 | try { 573 | String className = readFile(tempFile); 574 | if (className != null) { 575 | try { 576 | // Let's check if a class with the given name exists. 577 | return Class.forName(className); 578 | } catch (ClassNotFoundException ignored) { 579 | } 580 | } 581 | writeFile(tempFile, getClass().getName()); 582 | return getClass(); 583 | } catch (IOException e) { 584 | if (logFailedRequests) { 585 | plugin.getLogger().log(Level.WARNING, "Failed to get first bStats class!", e); 586 | } 587 | return null; 588 | } 589 | } 590 | 591 | /** 592 | * Gets the plugin specific data. This method is called using Reflection. 593 | * 594 | * @return The plugin specific data. 595 | */ 596 | public JsonObject getPluginData() { 597 | JsonObject data = new JsonObject(); 598 | 599 | String pluginName = plugin.getDescription().getName(); 600 | String pluginVersion = plugin.getDescription().getVersion(); 601 | 602 | data.addProperty("pluginName", pluginName); 603 | data.addProperty("id", pluginId); 604 | data.addProperty("pluginVersion", pluginVersion); 605 | 606 | JsonArray customCharts = new JsonArray(); 607 | for (CustomChart customChart : charts) { 608 | // Add the data of the custom charts 609 | JsonObject chart = customChart.getRequestJsonObject(plugin.getLogger(), logFailedRequests); 610 | if (chart == null) { // If the chart is null, we skip it 611 | continue; 612 | } 613 | customCharts.add(chart); 614 | } 615 | data.add("customCharts", customCharts); 616 | 617 | return data; 618 | } 619 | 620 | /** 621 | * Gets the server specific data. 622 | * 623 | * @return The server specific data. 624 | */ 625 | @SuppressWarnings("deprecation") 626 | private JsonObject getServerData() { 627 | // Minecraft specific data 628 | int playerAmount = Math.min(plugin.getProxy().getOnlineCount(), 500); 629 | int onlineMode = plugin.getProxy().getConfig().isOnlineMode() ? 1 : 0; 630 | String bungeecordVersion = plugin.getProxy().getVersion(); 631 | int managedServers = plugin.getProxy().getServers().size(); 632 | 633 | // OS/Java specific data 634 | String javaVersion = System.getProperty("java.version"); 635 | String osName = System.getProperty("os.name"); 636 | String osArch = System.getProperty("os.arch"); 637 | String osVersion = System.getProperty("os.version"); 638 | int coreCount = Runtime.getRuntime().availableProcessors(); 639 | 640 | JsonObject data = new JsonObject(); 641 | 642 | data.addProperty("serverUUID", serverUUID); 643 | 644 | data.addProperty("playerAmount", playerAmount); 645 | data.addProperty("managedServers", managedServers); 646 | data.addProperty("onlineMode", onlineMode); 647 | data.addProperty("bungeecordVersion", bungeecordVersion); 648 | 649 | data.addProperty("javaVersion", javaVersion); 650 | data.addProperty("osName", osName); 651 | data.addProperty("osArch", osArch); 652 | data.addProperty("osVersion", osVersion); 653 | data.addProperty("coreCount", coreCount); 654 | 655 | return data; 656 | } 657 | 658 | /** 659 | * Checks if bStats is enabled. 660 | * 661 | * @return Whether bStats is enabled or not. 662 | */ 663 | public boolean isEnabled() { 664 | return enabled; 665 | } 666 | 667 | /** 668 | * Loads the bStats configuration. 669 | * 670 | * @throws IOException If something did not work :( 671 | */ 672 | private void loadConfig() throws IOException { 673 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 674 | bStatsFolder.mkdirs(); 675 | File configFile = new File(bStatsFolder, "config.yml"); 676 | if (!configFile.exists()) { 677 | writeFile(configFile, 678 | "#bStats collects some data for plugin authors like how many servers are using their plugins.", 679 | "#To honor their work, you should not disable it.", 680 | "#This has nearly no effect on the server performance!", 681 | "#Check out https://bStats.org/ to learn more :)", "enabled: true", 682 | "serverUuid: \"" + UUID.randomUUID() + "\"", "logFailedRequests: false", "logSentData: false", 683 | "logResponseStatusText: false"); 684 | } 685 | 686 | Configuration configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile); 687 | 688 | // Load configuration 689 | enabled = configuration.getBoolean("enabled", true); 690 | serverUUID = configuration.getString("serverUuid"); 691 | logFailedRequests = configuration.getBoolean("logFailedRequests", false); 692 | logSentData = configuration.getBoolean("logSentData", false); 693 | logResponseStatusText = configuration.getBoolean("logResponseStatusText", false); 694 | } 695 | 696 | /** 697 | * Reads the first line of the file. 698 | * 699 | * @param file The file to read. Cannot be null. 700 | * @return The first line of the file or {@code null} if the file does not exist 701 | * or is empty. 702 | * @throws IOException If something did not work :( 703 | */ 704 | private String readFile(File file) throws IOException { 705 | if (!file.exists()) { 706 | return null; 707 | } 708 | try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) { 709 | return bufferedReader.readLine(); 710 | } 711 | } 712 | 713 | private void startSubmitting() { 714 | // The data collection is async, as well as sending the data 715 | // Bungeecord does not have a main thread, everything is async 716 | plugin.getProxy().getScheduler().schedule(plugin, this::submitData, 2, 30, TimeUnit.MINUTES); 717 | // Submit the data every 30 minutes, first time after 2 minutes to give other 718 | // plugins enough time to start 719 | // WARNING: Changing the frequency has no effect but your plugin WILL be 720 | // blocked/deleted! 721 | // WARNING: Just don't do it! 722 | } 723 | 724 | /** 725 | * Collects the data and sends it afterwards. 726 | */ 727 | private void submitData() { 728 | final JsonObject data = getServerData(); 729 | 730 | final JsonArray pluginData = new JsonArray(); 731 | // Search for all other bStats Metrics classes to get their plugin data 732 | for (Object metrics : knownMetricsInstances) { 733 | try { 734 | Object plugin = metrics.getClass().getMethod("getPluginData").invoke(metrics); 735 | if (plugin instanceof JsonObject) { 736 | pluginData.add((JsonObject) plugin); 737 | } 738 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { 739 | } 740 | } 741 | 742 | data.add("plugins", pluginData); 743 | 744 | try { 745 | // Send the data 746 | sendData(plugin, data); 747 | } catch (Exception e) { 748 | // Something went wrong! :( 749 | if (logFailedRequests) { 750 | plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats!", e); 751 | } 752 | } 753 | } 754 | 755 | /** 756 | * Writes a String to a file. It also adds a note for the user, 757 | * 758 | * @param file The file to write to. Cannot be null. 759 | * @param lines The lines to write. 760 | * @throws IOException If something did not work :( 761 | */ 762 | private void writeFile(File file, String... lines) throws IOException { 763 | try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))) { 764 | for (String line : lines) { 765 | bufferedWriter.write(line); 766 | bufferedWriter.newLine(); 767 | } 768 | } 769 | } 770 | 771 | } 772 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/Config.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.bungee; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.file.Files; 7 | import java.util.Set; 8 | 9 | import lombok.Getter; 10 | import net.md_5.bungee.config.Configuration; 11 | import net.md_5.bungee.config.ConfigurationProvider; 12 | import net.md_5.bungee.config.YamlConfiguration; 13 | 14 | public class Config { 15 | private VotifierPlusBungee bungee; 16 | @Getter 17 | private Configuration data; 18 | 19 | public Config(VotifierPlusBungee bungee) { 20 | this.bungee = bungee; 21 | } 22 | 23 | public void load() { 24 | if (!bungee.getDataFolder().exists()) 25 | bungee.getDataFolder().mkdir(); 26 | 27 | File file = new File(bungee.getDataFolder(), "bungeeconfig.yml"); 28 | 29 | if (!file.exists()) { 30 | try (InputStream in = bungee.getResourceAsStream("bungeeconfig.yml")) { 31 | Files.copy(in, file.toPath()); 32 | } catch (IOException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | try { 37 | data = ConfigurationProvider.getProvider(YamlConfiguration.class) 38 | .load(new File(bungee.getDataFolder(), "bungeeconfig.yml")); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | 44 | public void save() { 45 | try { 46 | ConfigurationProvider.getProvider(YamlConfiguration.class).save(data, 47 | new File(bungee.getDataFolder(), "bungeeconfig.yml")); 48 | } catch (IOException e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | 53 | public String getHost() { 54 | return getData().getString("host", ""); 55 | } 56 | 57 | public int getPort() { 58 | return getData().getInt("port"); 59 | } 60 | 61 | public boolean getDebug() { 62 | return getData().getBoolean("Debug", false); 63 | } 64 | 65 | public Set getServers() { 66 | return (Set) getData().getSection("Forwarding").getKeys(); 67 | } 68 | 69 | public Configuration getServerData(String s) { 70 | return getData().getSection("Forwarding." + s); 71 | } 72 | 73 | public Set getTokens() { 74 | return (Set) getData().getSection("tokens").getKeys(); 75 | } 76 | 77 | public boolean getTokenSupport() { 78 | return getData().getBoolean("TokenSupport", false); 79 | } 80 | 81 | public String getToken(String key) { 82 | return getData().getString("tokens." + key, null); 83 | } 84 | 85 | public boolean containsTokens() { 86 | return getData().contains("tokens"); 87 | } 88 | 89 | public void setToken(String key, String token) { 90 | getData().set("tokens." + key, token); 91 | save(); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusBungee.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.bungee; 2 | 3 | import java.io.File; 4 | import java.io.InputStreamReader; 5 | import java.io.Reader; 6 | import java.net.URL; 7 | import java.security.CodeSource; 8 | import java.security.Key; 9 | import java.security.KeyPair; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.zip.ZipEntry; 14 | import java.util.zip.ZipInputStream; 15 | 16 | import com.vexsoftware.votifier.ForwardServer; 17 | import com.vexsoftware.votifier.crypto.RSAIO; 18 | import com.vexsoftware.votifier.crypto.RSAKeygen; 19 | import com.vexsoftware.votifier.crypto.TokenUtil; 20 | import com.vexsoftware.votifier.model.Vote; 21 | import com.vexsoftware.votifier.net.VoteReceiver; 22 | 23 | import lombok.Getter; 24 | import lombok.Setter; 25 | import net.md_5.bungee.api.plugin.Plugin; 26 | import net.md_5.bungee.config.Configuration; 27 | import net.md_5.bungee.config.ConfigurationProvider; 28 | 29 | public class VotifierPlusBungee extends Plugin { 30 | private VotifierPlusBungee instance; 31 | @Getter 32 | private VoteReceiver voteReceiver; 33 | @Getter 34 | private Config config; 35 | @Getter 36 | @Setter 37 | private KeyPair keyPair; 38 | private String buildNumber; 39 | private Map tokens = new HashMap(); 40 | 41 | private void loadTokens() { 42 | tokens.clear(); 43 | if (!config.containsTokens()) { 44 | config.setToken("default", TokenUtil.newToken()); 45 | } 46 | 47 | for (String key : config.getTokens()) { 48 | tokens.put(key, TokenUtil.createKeyFrom(config.getToken(key))); 49 | } 50 | } 51 | 52 | @Override 53 | public void onEnable() { 54 | instance = this; 55 | config = new Config(this); 56 | config.load(); 57 | loadTokens(); 58 | getProxy().getPluginManager().registerCommand(this, new VotifierPlusCommand(this)); 59 | File rsaDirectory = new File(getDataFolder() + "/rsa"); 60 | 61 | /* 62 | * Create RSA directory and keys if it does not exist; otherwise, read keys. 63 | */ 64 | try { 65 | if (!rsaDirectory.exists()) { 66 | rsaDirectory.mkdir(); 67 | keyPair = RSAKeygen.generate(2048); 68 | RSAIO.save(rsaDirectory, keyPair); 69 | } else { 70 | keyPair = RSAIO.load(rsaDirectory); 71 | } 72 | } catch (Exception ex) { 73 | getLogger().severe("Error reading configuration file or RSA keys"); 74 | return; 75 | } 76 | 77 | BStatsMetricsBungee metrics = new BStatsMetricsBungee(this, 20283); 78 | loadVersionFile(); 79 | if (!buildNumber.equals("NOTSET")) { 80 | metrics.addCustomChart(new BStatsMetricsBungee.SimplePie("dev_build_number", () -> "" + buildNumber)); 81 | } 82 | 83 | loadVoteReceiver(); 84 | } 85 | 86 | private Configuration getVersionFile() { 87 | try { 88 | CodeSource src = this.getClass().getProtectionDomain().getCodeSource(); 89 | if (src != null) { 90 | URL jar = src.getLocation(); 91 | ZipInputStream zip = null; 92 | zip = new ZipInputStream(jar.openStream()); 93 | while (true) { 94 | ZipEntry e = zip.getNextEntry(); 95 | if (e != null) { 96 | String name = e.getName(); 97 | if (name.equals("votifierplusversion.yml")) { 98 | Reader defConfigStream = new InputStreamReader(zip); 99 | if (defConfigStream != null) { 100 | Configuration conf = ConfigurationProvider 101 | .getProvider(net.md_5.bungee.config.YamlConfiguration.class) 102 | .load(defConfigStream); 103 | 104 | defConfigStream.close(); 105 | return conf; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | return null; 115 | } 116 | 117 | public void loadVersionFile() { 118 | Configuration conf = getVersionFile(); 119 | if (conf != null) { 120 | buildNumber = conf.get("buildnumber", "NOTSET"); 121 | } 122 | } 123 | 124 | public void reload() { 125 | config.load(); 126 | loadTokens(); 127 | loadVoteReceiver(); 128 | } 129 | 130 | private void loadVoteReceiver() { 131 | try { 132 | voteReceiver = new VoteReceiver(config.getHost(), config.getPort()) { 133 | 134 | @Override 135 | public void logWarning(String warn) { 136 | getLogger().warning(warn); 137 | } 138 | 139 | @Override 140 | public void logSevere(String msg) { 141 | getLogger().severe(msg); 142 | } 143 | 144 | @Override 145 | public void log(String msg) { 146 | getLogger().info(msg); 147 | } 148 | 149 | @Override 150 | public String getVersion() { 151 | return getDescription().getVersion(); 152 | } 153 | 154 | @Override 155 | public Set getServers() { 156 | return config.getServers(); 157 | } 158 | 159 | @Override 160 | public ForwardServer getServerData(String s) { 161 | Configuration d = config.getServerData(s); 162 | return new ForwardServer(d.getBoolean("Enabled"), d.getString("Host", ""), d.getInt("Port"), 163 | d.getString("Key", "")); 164 | } 165 | 166 | @Override 167 | public KeyPair getKeyPair() { 168 | return instance.getKeyPair(); 169 | } 170 | 171 | @Override 172 | public void debug(Exception e) { 173 | if (config.getDebug()) { 174 | e.printStackTrace(); 175 | } 176 | } 177 | 178 | @Override 179 | public void debug(String debug) { 180 | if (config.getDebug()) { 181 | getLogger().info("Debug: " + debug); 182 | } 183 | } 184 | 185 | @Override 186 | public void callEvent(Vote vote) { 187 | getProxy().getPluginManager() 188 | .callEvent(new com.vexsoftware.votifier.bungee.events.VotifierEvent(vote)); 189 | } 190 | 191 | @Override 192 | public Map getTokens() { 193 | return tokens; 194 | } 195 | 196 | @Override 197 | public boolean isUseTokens() { 198 | return config.getTokenSupport(); 199 | } 200 | }; 201 | voteReceiver.start(); 202 | 203 | getLogger().info("Votifier enabled."); 204 | } catch (Exception ex) { 205 | return; 206 | } 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/VotifierPlusCommand.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.bungee; 2 | 3 | import java.io.File; 4 | import java.io.OutputStream; 5 | import java.net.InetSocketAddress; 6 | import java.net.Socket; 7 | import java.net.SocketAddress; 8 | import java.security.PublicKey; 9 | 10 | import com.vexsoftware.votifier.crypto.RSAIO; 11 | import com.vexsoftware.votifier.crypto.RSAKeygen; 12 | 13 | import net.md_5.bungee.api.CommandSender; 14 | import net.md_5.bungee.api.chat.TextComponent; 15 | import net.md_5.bungee.api.plugin.Command; 16 | 17 | public class VotifierPlusCommand extends Command { 18 | private VotifierPlusBungee bungee; 19 | 20 | public VotifierPlusCommand(VotifierPlusBungee bungee) { 21 | super("votifierplusbungee", "votifierplus.admin"); 22 | this.bungee = bungee; 23 | } 24 | 25 | @Override 26 | public void execute(CommandSender sender, String[] args) { 27 | if (sender.hasPermission("votifierplus.admin")) { 28 | if (args.length > 0) { 29 | if (args[0].equalsIgnoreCase("reload")) { 30 | bungee.reload(); 31 | sender.sendMessage(new TextComponent("Reloading VotifierPlus")); 32 | } 33 | if (args[0].equalsIgnoreCase("GenerateKeys")) { 34 | File rsaDirectory = new File(bungee.getDataFolder() + File.separator + "rsa"); 35 | 36 | try { 37 | for (File file : rsaDirectory.listFiles()) { 38 | if (!file.isDirectory()) { 39 | file.delete(); 40 | } 41 | } 42 | rsaDirectory.mkdir(); 43 | bungee.setKeyPair(RSAKeygen.generate(2048)); 44 | RSAIO.save(rsaDirectory, bungee.getKeyPair()); 45 | } catch (Exception ex) { 46 | sender.sendMessage(new TextComponent("Failed to create keys")); 47 | return; 48 | } 49 | sender.sendMessage(new TextComponent("New keys generated")); 50 | } 51 | if (args[0].equalsIgnoreCase("vote") && args.length > 2) { 52 | try { 53 | PublicKey publicKey = bungee.getKeyPair().getPublic(); 54 | String serverIP = bungee.getConfig().getHost(); 55 | int serverPort = bungee.getConfig().getPort(); 56 | 57 | String VoteString = "VOTE\n" + args[2] + "\n" + args[1] + "\n" + "Address" + "\n" + "TestVote" 58 | + "\n"; 59 | 60 | SocketAddress sockAddr = new InetSocketAddress(serverIP, serverPort); 61 | Socket socket1 = new Socket(); 62 | socket1.connect(sockAddr, 1000); 63 | OutputStream socketOutputStream = socket1.getOutputStream(); 64 | socketOutputStream.write(bungee.getVoteReceiver().encrypt(VoteString.getBytes(), publicKey)); 65 | socketOutputStream.close(); 66 | socket1.close(); 67 | sender.sendMessage(new TextComponent("Vote triggered")); 68 | 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | 72 | } 73 | 74 | } 75 | 76 | } 77 | } else { 78 | sender.sendMessage(new TextComponent("You do not have permission to do this!")); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/bungee/events/VotifierEvent.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.bungee.events; 2 | 3 | import com.vexsoftware.votifier.model.Vote; 4 | 5 | import net.md_5.bungee.api.plugin.Event; 6 | 7 | /** 8 | * {@code VotifierEvent} is a custom Bukkit event class that is sent 9 | * synchronously to CraftBukkit's main thread allowing other plugins to listener 10 | * for votes. 11 | * 12 | * @author frelling 13 | * 14 | */ 15 | public class VotifierEvent extends Event { 16 | 17 | /** 18 | * Encapsulated vote record. 19 | */ 20 | private Vote vote; 21 | 22 | /** 23 | * Constructs a vote event that encapsulated the given vote record. 24 | * 25 | * @param vote 26 | * vote record 27 | */ 28 | public VotifierEvent(final Vote vote) { 29 | this.vote = vote; 30 | } 31 | 32 | /** 33 | * Return the encapsulated vote record. 34 | * 35 | * @return vote record 36 | */ 37 | public Vote getVote() { 38 | return vote; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/commands/CommandLoader.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.commands; 2 | 3 | import java.io.File; 4 | import java.io.OutputStream; 5 | import java.net.InetSocketAddress; 6 | import java.net.Socket; 7 | import java.net.SocketAddress; 8 | import java.security.PublicKey; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | 13 | import org.bukkit.command.CommandSender; 14 | 15 | import com.bencodez.simpleapi.command.CommandHandler; 16 | import com.bencodez.simpleapi.scheduler.BukkitScheduler; 17 | import com.vexsoftware.votifier.VotifierPlus; 18 | import com.vexsoftware.votifier.crypto.RSAIO; 19 | import com.vexsoftware.votifier.crypto.RSAKeygen; 20 | 21 | import net.md_5.bungee.api.ChatColor; 22 | import net.md_5.bungee.api.chat.TextComponent; 23 | 24 | // TODO: Auto-generated Javadoc 25 | /** 26 | * The Class CommandLoader. 27 | */ 28 | public class CommandLoader { 29 | 30 | private static CommandLoader instance = new CommandLoader(); 31 | private VotifierPlus plugin = VotifierPlus.getInstance(); 32 | 33 | public static CommandLoader getInstance() { 34 | return instance; 35 | } 36 | 37 | public ArrayList helpText(CommandSender sender) { 38 | ArrayList msg = new ArrayList(); 39 | HashMap unsorted = new HashMap(); 40 | 41 | boolean requirePerms = false; 42 | ChatColor hoverColor = ChatColor.AQUA; 43 | for (CommandHandler cmdHandle : plugin.getCommands()) { 44 | if (!requirePerms || cmdHandle.hasPerm(sender)) { 45 | unsorted.put(cmdHandle.getHelpLineCommand("/votifierplus"), 46 | cmdHandle.getHelpLine("/votifierplus", "&6%Command% - &6%HelpMessage%", hoverColor)); 47 | } 48 | } 49 | ArrayList unsortedList = new ArrayList(); 50 | unsortedList.addAll(unsorted.keySet()); 51 | Collections.sort(unsortedList, String.CASE_INSENSITIVE_ORDER); 52 | for (String cmd : unsortedList) { 53 | msg.add(unsorted.get(cmd)); 54 | } 55 | 56 | return msg; 57 | } 58 | 59 | public void loadCommands() { 60 | plugin.getCommands() 61 | .add(new CommandHandler(plugin, new String[] { "Help" }, "VotifierPlus.Help", "Open help page") { 62 | 63 | @Override 64 | public void execute(CommandSender sender, String[] args) { 65 | sendMessageJson(sender, helpText(sender)); 66 | } 67 | 68 | @Override 69 | public String getHelpLine() { 70 | return plugin.getConfigFile().getHelpLine(); 71 | } 72 | 73 | @Override 74 | public void debug(String debug) { 75 | plugin.debug(debug); 76 | } 77 | 78 | @Override 79 | public String formatNotNumber() { 80 | return plugin.getConfigFile().getFormatNotNumber(); 81 | } 82 | 83 | @Override 84 | public String formatNoPerms() { 85 | return plugin.getConfigFile().getFormatNoPerms(); 86 | } 87 | 88 | @Override 89 | public BukkitScheduler getBukkitScheduler() { 90 | return plugin.getBukkitScheduler(); 91 | } 92 | }); 93 | plugin.getCommands() 94 | .add(new CommandHandler(plugin, new String[] { "Reload" }, "VotifierPlus.Reload", "Reload the plugin") { 95 | 96 | @Override 97 | public void execute(CommandSender sender, String[] args) { 98 | plugin.reload(); 99 | sendMessage(sender, "&cVotifierPlus " + plugin.getDescription().getVersion() + " reloaded"); 100 | } 101 | 102 | @Override 103 | public String getHelpLine() { 104 | return plugin.getConfigFile().getHelpLine(); 105 | } 106 | 107 | @Override 108 | public void debug(String debug) { 109 | plugin.debug(debug); 110 | } 111 | 112 | @Override 113 | public String formatNotNumber() { 114 | return plugin.getConfigFile().getFormatNotNumber(); 115 | } 116 | 117 | @Override 118 | public String formatNoPerms() { 119 | return plugin.getConfigFile().getFormatNoPerms(); 120 | } 121 | 122 | @Override 123 | public BukkitScheduler getBukkitScheduler() { 124 | return plugin.getBukkitScheduler(); 125 | } 126 | }); 127 | 128 | plugin.getCommands().add(new CommandHandler(plugin, new String[] { "GenerateKeys" }, 129 | "VotifierPlus.GenerateKeys", "Regenerate votifier keys", true, true) { 130 | 131 | @Override 132 | public void execute(CommandSender sender, String[] args) { 133 | File rsaDirectory = new File(plugin.getDataFolder() + File.separator + "rsa"); 134 | 135 | try { 136 | for (File file : rsaDirectory.listFiles()) { 137 | if (!file.isDirectory()) { 138 | file.delete(); 139 | } 140 | } 141 | rsaDirectory.mkdir(); 142 | plugin.setKeyPair(RSAKeygen.generate(2048)); 143 | RSAIO.save(rsaDirectory, plugin.getKeyPair()); 144 | } catch (Exception ex) { 145 | sendMessage(sender, "&cFailed to create keys"); 146 | return; 147 | } 148 | sendMessage(sender, "&cNew keys generated"); 149 | } 150 | 151 | @Override 152 | public String getHelpLine() { 153 | return plugin.getConfigFile().getHelpLine(); 154 | } 155 | 156 | @Override 157 | public void debug(String debug) { 158 | plugin.debug(debug); 159 | } 160 | 161 | @Override 162 | public String formatNotNumber() { 163 | return plugin.getConfigFile().getFormatNotNumber(); 164 | } 165 | 166 | @Override 167 | public String formatNoPerms() { 168 | return plugin.getConfigFile().getFormatNoPerms(); 169 | } 170 | 171 | @Override 172 | public BukkitScheduler getBukkitScheduler() { 173 | return plugin.getBukkitScheduler(); 174 | } 175 | }); 176 | 177 | plugin.getCommands().add(new CommandHandler(plugin, new String[] { "Test", "(player)", "(Text)" }, 178 | "VotifierPlus.Test", "Test votifier connection") { 179 | 180 | @Override 181 | public void execute(CommandSender sender, String[] args) { 182 | try { 183 | PublicKey publicKey = plugin.getKeyPair().getPublic(); 184 | String serverIP = plugin.configFile.getHost(); 185 | int serverPort = plugin.configFile.getPort(); 186 | if (serverIP.length() != 0) { 187 | String VoteString = "VOTE\n" + args[2] + "\n" + args[1] + "\n" + "Address" + "\n" + "TestVote" 188 | + "\n"; 189 | 190 | SocketAddress sockAddr = new InetSocketAddress(serverIP, serverPort); 191 | Socket socket1 = new Socket(); 192 | socket1.connect(sockAddr, 1000); 193 | OutputStream socketOutputStream = socket1.getOutputStream(); 194 | socketOutputStream.write(plugin.getVoteReceiver().encrypt(VoteString.getBytes(), publicKey)); 195 | socketOutputStream.close(); 196 | socket1.close(); 197 | } 198 | } catch (Exception e) { 199 | e.printStackTrace(); 200 | } 201 | sendMessage(sender, "&cCheck console for test results"); 202 | 203 | } 204 | 205 | @Override 206 | public String getHelpLine() { 207 | return plugin.getConfigFile().getHelpLine(); 208 | } 209 | 210 | @Override 211 | public void debug(String debug) { 212 | plugin.debug(debug); 213 | } 214 | 215 | @Override 216 | public String formatNotNumber() { 217 | return plugin.getConfigFile().getFormatNotNumber(); 218 | } 219 | 220 | @Override 221 | public String formatNoPerms() { 222 | return plugin.getConfigFile().getFormatNoPerms(); 223 | } 224 | 225 | @Override 226 | public BukkitScheduler getBukkitScheduler() { 227 | return plugin.getBukkitScheduler(); 228 | } 229 | 230 | }); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/commands/CommandVotifierPlus.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.commands; 2 | 3 | import org.bukkit.ChatColor; 4 | import org.bukkit.command.Command; 5 | import org.bukkit.command.CommandExecutor; 6 | import org.bukkit.command.CommandSender; 7 | 8 | import com.bencodez.simpleapi.command.CommandHandler; 9 | import com.vexsoftware.votifier.VotifierPlus; 10 | 11 | // TODO: Auto-generated Javadoc 12 | /** 13 | * The Class CommandVotifierPlus. 14 | */ 15 | public class CommandVotifierPlus implements CommandExecutor { 16 | /** The plugin. */ 17 | private VotifierPlus plugin; 18 | 19 | /** 20 | * Instantiates a new command vote. 21 | * 22 | * @param plugin 23 | * the plugin 24 | */ 25 | public CommandVotifierPlus(VotifierPlus plugin) { 26 | this.plugin = plugin; 27 | } 28 | 29 | /* 30 | * (non-Javadoc) 31 | * @see org.bukkit.command.CommandExecutor#onCommand(org.bukkit.command. 32 | * CommandSender , org.bukkit.command.Command, java.lang.String, 33 | * java.lang.String[]) 34 | */ 35 | @Override 36 | public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { 37 | 38 | for (CommandHandler commandHandler : plugin.getCommands()) { 39 | if (commandHandler.runCommand(sender, args)) { 40 | return true; 41 | } 42 | } 43 | 44 | // invalid command 45 | sender.sendMessage(ChatColor.RED + "No valid arguments, see /votifierplus help!"); 46 | return true; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/commands/VotifierPlusTabCompleter.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.commands; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import org.bukkit.command.Command; 10 | import org.bukkit.command.CommandSender; 11 | import org.bukkit.command.TabCompleter; 12 | 13 | import com.bencodez.simpleapi.command.TabCompleteHandler; 14 | import com.bencodez.simpleapi.messages.MessageAPI; 15 | import com.vexsoftware.votifier.VotifierPlus; 16 | 17 | // TODO: Auto-generated Javadoc 18 | /** 19 | * The Class VoteTabCompleter. 20 | */ 21 | public class VotifierPlusTabCompleter implements TabCompleter { 22 | 23 | /* 24 | * (non-Javadoc) 25 | * @see org.bukkit.command.TabCompleter#onTabComplete(org.bukkit.command. 26 | * CommandSender, org.bukkit.command.Command, java.lang.String, 27 | * java.lang.String[]) 28 | */ 29 | @Override 30 | public List onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) { 31 | 32 | ArrayList tab = new ArrayList(); 33 | 34 | Set cmds = new HashSet(); 35 | 36 | cmds.addAll(TabCompleteHandler.getInstance().getTabCompleteOptions(VotifierPlus.getInstance().getCommands(), 37 | sender, args, args.length - 1)); 38 | 39 | for (String str : cmds) { 40 | if (MessageAPI.startsWithIgnoreCase(str, args[args.length - 1])) { 41 | tab.add(str); 42 | } 43 | } 44 | 45 | Collections.sort(tab, String.CASE_INSENSITIVE_ORDER); 46 | 47 | return tab; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/config/Config.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.config; 2 | 3 | import java.io.File; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.bukkit.configuration.ConfigurationSection; 8 | 9 | import com.bencodez.simpleapi.debug.DebugLevel; 10 | import com.bencodez.simpleapi.file.YMLFile; 11 | import com.bencodez.simpleapi.file.annotation.AnnotationHandler; 12 | import com.bencodez.simpleapi.file.annotation.ConfigDataBoolean; 13 | import com.bencodez.simpleapi.file.annotation.ConfigDataInt; 14 | import com.bencodez.simpleapi.file.annotation.ConfigDataKeys; 15 | import com.bencodez.simpleapi.file.annotation.ConfigDataString; 16 | import com.vexsoftware.votifier.VotifierPlus; 17 | 18 | import lombok.Getter; 19 | import lombok.Setter; 20 | 21 | public class Config extends YMLFile { 22 | 23 | public Config(VotifierPlus plugin) { 24 | super(plugin, new File(VotifierPlus.getInstance().getDataFolder(), "config.yml")); 25 | } 26 | 27 | public void loadValues() { 28 | new AnnotationHandler().load(getData(), this); 29 | debug = DebugLevel.getDebug(debugLevelStr); 30 | } 31 | 32 | @Override 33 | public void onFileCreation() { 34 | VotifierPlus.getInstance().saveResource("config.yml", true); 35 | } 36 | 37 | @ConfigDataString(path = "host") 38 | @Getter 39 | @Setter 40 | private String host = "0.0.0.0"; 41 | 42 | @ConfigDataInt(path = "port") 43 | @Getter 44 | @Setter 45 | private int port = 8192; 46 | 47 | @ConfigDataString(path = "DebugLevel", options = { "NONE", "INFO", "EXTRA", "DEV" }) 48 | private String debugLevelStr = "NONE"; 49 | 50 | @Getter 51 | @Setter 52 | private DebugLevel debug = DebugLevel.NONE; 53 | 54 | @ConfigDataKeys(path = "Forwarding") 55 | @Getter 56 | @Setter 57 | private Set servers = new HashSet(); 58 | 59 | @Getter 60 | @Setter 61 | @ConfigDataString(path = "Format.NoPerms") 62 | private String formatNoPerms = "&cYou do not have enough permission!"; 63 | 64 | @Getter 65 | @Setter 66 | @ConfigDataString(path = "Format.NotNumber") 67 | private String formatNotNumber = "&cError on &6%arg%&c, number expected!"; 68 | 69 | @Getter 70 | @Setter 71 | @ConfigDataString(path = "Format.HelpLine") 72 | private String helpLine = "&3&l%Command% - &3%HelpMessage%"; 73 | 74 | @ConfigDataBoolean(path = "DisableUpdateChecking") 75 | @Getter 76 | private boolean disableUpdateChecking = false; 77 | 78 | @ConfigDataBoolean(path = "TokenSupport") 79 | @Getter 80 | @Setter 81 | private boolean tokenSupport = false; 82 | 83 | public ConfigurationSection getForwardingConfiguration(String s) { 84 | return getData().getConfigurationSection("Forwarding." + s); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/crypto/RSA.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | */ 18 | 19 | package com.vexsoftware.votifier.crypto; 20 | 21 | import java.security.PrivateKey; 22 | import java.security.PublicKey; 23 | 24 | import javax.crypto.Cipher; 25 | 26 | /** 27 | * Static RSA utility methods for encrypting and decrypting blocks of 28 | * information. 29 | * 30 | * @author Blake Beaupain 31 | */ 32 | public class RSA { 33 | 34 | /** 35 | * Encrypts a block of data. 36 | * 37 | * @param data 38 | * The data to encrypt 39 | * @param key 40 | * The key to encrypt with 41 | * @return The encrypted data 42 | * @throws Exception 43 | * If an error occurs 44 | */ 45 | public static byte[] encrypt(byte[] data, PublicKey key) throws Exception { 46 | Cipher cipher = Cipher.getInstance("RSA"); 47 | cipher.init(Cipher.ENCRYPT_MODE, key); 48 | return cipher.doFinal(data); 49 | } 50 | 51 | /** 52 | * Decrypts a block of data. 53 | * 54 | * @param data 55 | * The data to decrypt 56 | * @param key 57 | * The key to decrypt with 58 | * @return The decrypted data 59 | * @throws Exception 60 | * If an error occurs 61 | */ 62 | public static byte[] decrypt(byte[] data, PrivateKey key) throws Exception { 63 | Cipher cipher = Cipher.getInstance("RSA"); 64 | cipher.init(Cipher.DECRYPT_MODE, key); 65 | return cipher.doFinal(data); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | */ 18 | 19 | package com.vexsoftware.votifier.crypto; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.FileOutputStream; 24 | import java.security.KeyFactory; 25 | import java.security.KeyPair; 26 | import java.security.PrivateKey; 27 | import java.security.PublicKey; 28 | import java.security.spec.PKCS8EncodedKeySpec; 29 | import java.security.spec.X509EncodedKeySpec; 30 | import java.util.Base64; 31 | 32 | /** 33 | * Static utility methods for saving and loading RSA key pairs. 34 | * 35 | * @author Blake Beaupain 36 | */ 37 | public class RSAIO { 38 | 39 | /** 40 | * Saves the key pair to the disk. 41 | * 42 | * @param directory 43 | * The directory to save to 44 | * @param keyPair 45 | * The key pair to save 46 | * @throws Exception 47 | * If an error occurs 48 | */ 49 | public static void save(File directory, KeyPair keyPair) throws Exception { 50 | PrivateKey privateKey = keyPair.getPrivate(); 51 | PublicKey publicKey = keyPair.getPublic(); 52 | 53 | // Store the public key. 54 | X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKey.getEncoded()); 55 | FileOutputStream out = null; 56 | try { 57 | out = new FileOutputStream(directory + "/public.key"); 58 | out.write(Base64.getEncoder().encodeToString(publicSpec.getEncoded()).getBytes()); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } finally { 62 | if (out != null) { 63 | out.close(); 64 | } 65 | } 66 | // out.close(); 67 | 68 | // Store the private key. 69 | PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); 70 | try { 71 | out = new FileOutputStream(directory + "/private.key"); 72 | out.write(Base64.getEncoder().encodeToString(privateSpec.getEncoded()).getBytes()); 73 | } catch (Exception e) { 74 | e.printStackTrace(); 75 | } finally { 76 | if (out != null) { 77 | out.close(); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Loads an RSA key pair from a directory. The directory must have the files 84 | * "public.key" and "private.key". 85 | * 86 | * @param directory 87 | * The directory to load from 88 | * @return The key pair 89 | * @throws Exception 90 | * If an error occurs 91 | */ 92 | public static KeyPair load(File directory) throws Exception { 93 | // Read the public key file. 94 | File publicKeyFile = new File(directory + "/public.key"); 95 | byte[] encodedPublicKey = null; 96 | try (FileInputStream in = new FileInputStream(publicKeyFile)) { 97 | encodedPublicKey = new byte[(int) publicKeyFile.length()]; 98 | in.read(encodedPublicKey); 99 | encodedPublicKey = Base64.getDecoder().decode(encodedPublicKey); 100 | } catch (Exception e) { 101 | e.printStackTrace(); 102 | } 103 | 104 | // Read the private key file. 105 | File privateKeyFile = new File(directory + "/private.key"); 106 | byte[] encodedPrivateKey = null; 107 | try (FileInputStream in = new FileInputStream(privateKeyFile)) { 108 | encodedPrivateKey = new byte[(int) privateKeyFile.length()]; 109 | in.read(encodedPrivateKey); 110 | encodedPrivateKey = Base64.getDecoder().decode(encodedPrivateKey); 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | 115 | // Instantiate and return the key pair. 116 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 117 | X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey); 118 | PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); 119 | PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); 120 | PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); 121 | return new KeyPair(publicKey, privateKey); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/crypto/RSAKeygen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | */ 18 | 19 | package com.vexsoftware.votifier.crypto; 20 | 21 | import java.security.KeyPair; 22 | import java.security.KeyPairGenerator; 23 | import java.security.spec.RSAKeyGenParameterSpec; 24 | 25 | /** 26 | * An RSA key pair generator. 27 | * 28 | * @author Blake Beaupain 29 | */ 30 | public abstract class RSAKeygen { 31 | 32 | /** 33 | * Generates an RSA key pair. 34 | * 35 | * @param bits 36 | * The amount of bits 37 | * @return The key pair 38 | * @throws Exception 39 | * exception 40 | */ 41 | public static KeyPair generate(int bits) throws Exception { 42 | KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); 43 | RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(bits, RSAKeyGenParameterSpec.F4); 44 | keygen.initialize(spec); 45 | return keygen.generateKeyPair(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/crypto/TokenUtil.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.crypto; 2 | 3 | import java.math.BigInteger; 4 | import java.nio.charset.StandardCharsets; 5 | import java.security.Key; 6 | import java.security.SecureRandom; 7 | 8 | import javax.crypto.spec.SecretKeySpec; 9 | 10 | public class TokenUtil { 11 | private static final SecureRandom RANDOM = new SecureRandom(); 12 | 13 | public static String newToken() { 14 | return new BigInteger(130, RANDOM).toString(32); 15 | } 16 | 17 | public static Key createKeyFrom(String token) { 18 | return new SecretKeySpec(token.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/model/Vote.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | */ 18 | 19 | package com.vexsoftware.votifier.model; 20 | 21 | import lombok.Getter; 22 | import lombok.Setter; 23 | 24 | /** 25 | * A model for a vote. 26 | * 27 | * @author Blake Beaupain 28 | */ 29 | public class Vote { 30 | 31 | /** The name of the vote service. */ 32 | private String serviceName; 33 | 34 | /** The username of the voter. */ 35 | private String username; 36 | 37 | /** The address of the voter. */ 38 | private String address; 39 | 40 | /** The date and time of the vote. */ 41 | private String timeStamp; 42 | 43 | @Getter 44 | @Setter 45 | /** source of the connection of this vote */ 46 | private String sourceAddress; 47 | 48 | public Vote(String serviceName, String username, String address, String timeStamp) { 49 | this.serviceName = serviceName; 50 | this.username = username; 51 | this.address = address; 52 | this.timeStamp = timeStamp; 53 | } 54 | 55 | public Vote() { 56 | 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Vote (from:" + serviceName + " username:" + username + " address:" + address + " timeStamp:" + timeStamp 62 | + ", sourceAddress:" + sourceAddress + ")"; 63 | } 64 | 65 | /** 66 | * Sets the serviceName. 67 | * 68 | * @param serviceName The new serviceName 69 | */ 70 | public void setServiceName(String serviceName) { 71 | this.serviceName = serviceName; 72 | } 73 | 74 | /** 75 | * Gets the serviceName. 76 | * 77 | * @return The serviceName 78 | */ 79 | public String getServiceName() { 80 | return serviceName; 81 | } 82 | 83 | /** 84 | * Sets the username. 85 | * 86 | * @param username The new username 87 | */ 88 | public void setUsername(String username) { 89 | this.username = username.length() <= 16 ? username : username.substring(0, 16); 90 | } 91 | 92 | /** 93 | * Gets the username. 94 | * 95 | * @return The username 96 | */ 97 | public String getUsername() { 98 | return username; 99 | } 100 | 101 | /** 102 | * Sets the address. 103 | * 104 | * @param address The new address 105 | */ 106 | public void setAddress(String address) { 107 | this.address = address; 108 | } 109 | 110 | /** 111 | * Gets the address. 112 | * 113 | * @return The address 114 | */ 115 | public String getAddress() { 116 | return address; 117 | } 118 | 119 | /** 120 | * Sets the time stamp. 121 | * 122 | * @param timeStamp The new time stamp 123 | */ 124 | public void setTimeStamp(String timeStamp) { 125 | this.timeStamp = timeStamp; 126 | } 127 | 128 | /** 129 | * Gets the time stamp. 130 | * 131 | * @return The time stamp 132 | */ 133 | public String getTimeStamp() { 134 | return timeStamp; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/model/VotifierEvent.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.model; 2 | 3 | import org.bukkit.event.*; 4 | 5 | /** 6 | * {@code VotifierEvent} is a custom Bukkit event class that is sent 7 | * synchronously to CraftBukkit's main thread allowing other plugins to listener 8 | * for votes. 9 | * 10 | * @author frelling 11 | * 12 | */ 13 | public class VotifierEvent extends Event { 14 | /** 15 | * Event listener handler list. 16 | */ 17 | private static final HandlerList handlers = new HandlerList(); 18 | 19 | /** 20 | * Encapsulated vote record. 21 | */ 22 | private Vote vote; 23 | 24 | /** 25 | * Constructs a vote event that encapsulated the given vote record. 26 | * 27 | * @param vote 28 | * vote record 29 | */ 30 | public VotifierEvent(final Vote vote) { 31 | this.vote = vote; 32 | } 33 | 34 | /** 35 | * Return the encapsulated vote record. 36 | * 37 | * @return vote record 38 | */ 39 | public Vote getVote() { 40 | return vote; 41 | } 42 | 43 | @Override 44 | public HandlerList getHandlers() { 45 | return handlers; 46 | } 47 | 48 | public static HandlerList getHandlerList() { 49 | return handlers; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 Vex Software LLC 3 | * This file is part of Votifier. 4 | * 5 | * Votifier is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Votifier is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Votifier. If not, see . 17 | * 18 | * Modified to support handling of extra proxy protocol data (e.g. from HAProxy). 19 | * This version supports multiple connection wrappers: 20 | * 1. Direct TCP (no extra header) 21 | * 2. PROXY protocol v1 (text-based): if the data begins with "PROXY", read and discard that header line, 22 | * then drain any extra CR/LF characters. 23 | * 3. PROXY protocol v2 (binary): if the first 12 bytes match the v2 signature, 24 | * read and discard the full binary header. 25 | * 4. HTTP CONNECT tunneling: if the connection begins with "CONNECT", read/discard the CONNECT request 26 | * and send a "200 Connection Established" response. 27 | * 28 | * After discarding any extra header, the normal vote protocol is performed. 29 | * 30 | * Modified by: BenCodez / [Your Name] 31 | * 32 | * This modified version supports both legacy V1 vote blocks (RSA encrypted fixed 256-byte blocks) 33 | * and V2 token-based vote blocks sent in cleartext. 34 | * In V1 mode, vote fields are separated by newline ("\n") and processed using a position pointer; 35 | * in V2 mode, the vote payload must be JSON-formatted. 36 | * The handshake is sent as: "VOTIFIER 2 " 37 | */ 38 | package com.vexsoftware.votifier.net; 39 | 40 | import java.io.BufferedWriter; 41 | import java.io.ByteArrayOutputStream; 42 | import java.io.IOException; 43 | import java.io.OutputStream; 44 | import java.io.OutputStreamWriter; 45 | import java.io.PushbackInputStream; 46 | import java.net.InetSocketAddress; 47 | import java.net.ServerSocket; 48 | import java.net.Socket; 49 | import java.net.SocketAddress; 50 | import java.net.SocketException; 51 | import java.net.SocketTimeoutException; 52 | import java.nio.charset.StandardCharsets; 53 | import java.security.Key; 54 | import java.security.KeyFactory; 55 | import java.security.KeyPair; 56 | import java.security.PublicKey; 57 | import java.security.spec.X509EncodedKeySpec; 58 | import java.util.Base64; 59 | import java.util.Map; 60 | import java.util.Set; 61 | 62 | import javax.crypto.BadPaddingException; 63 | import javax.crypto.Cipher; 64 | import javax.crypto.Mac; 65 | import javax.crypto.spec.SecretKeySpec; 66 | 67 | import com.google.gson.Gson; 68 | import com.google.gson.JsonArray; 69 | import com.google.gson.JsonObject; 70 | import com.google.gson.stream.MalformedJsonException; 71 | import com.vexsoftware.votifier.ForwardServer; 72 | import com.vexsoftware.votifier.crypto.RSA; 73 | import com.vexsoftware.votifier.crypto.TokenUtil; 74 | import com.vexsoftware.votifier.model.Vote; 75 | 76 | import io.netty.buffer.ByteBuf; 77 | import io.netty.buffer.Unpooled; 78 | import lombok.Getter; 79 | 80 | public abstract class VoteReceiver extends Thread { 81 | 82 | private final String host; 83 | private final int port; 84 | @Getter 85 | private ServerSocket server; 86 | private boolean running = true; 87 | 88 | // Expected 12-byte signature for PROXY protocol v2. 89 | private static final byte[] PROXY_V2_SIGNATURE = new byte[] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 90 | 0x49, 0x54, 0x0A }; 91 | 92 | private static final Gson gson = new Gson(); 93 | 94 | public VoteReceiver(String host, int port) throws Exception { 95 | super("Votifier I/O"); 96 | this.host = host; 97 | this.port = port; 98 | setPriority(Thread.MIN_PRIORITY); 99 | initialize(); 100 | } 101 | 102 | public void initialize() throws Exception { 103 | try { 104 | server = new ServerSocket(); 105 | server.bind(new InetSocketAddress(host, port)); 106 | debug("Bound to " + server.getInetAddress().getHostAddress() + ":" + server.getLocalPort()); 107 | } catch (Exception ex) { 108 | logSevere( 109 | "Error initializing vote receiver. Please verify that the configured IP address and port are not already in use."); 110 | ex.printStackTrace(); 111 | throw new Exception(ex); 112 | } 113 | } 114 | 115 | public void shutdown() { 116 | running = false; 117 | if (server == null) 118 | return; 119 | try { 120 | server.close(); 121 | } catch (Exception ex) { 122 | logWarning("Unable to shut down vote receiver cleanly."); 123 | } 124 | } 125 | 126 | public abstract boolean isUseTokens(); 127 | 128 | /** 129 | * Enum representing the vote protocol version. 130 | */ 131 | private enum VoteProtocolVersion { 132 | V1, V2; 133 | } 134 | 135 | private static final short PROTOCOL_2_MAGIC = 0x733A; 136 | 137 | /** 138 | * Checks if the incoming vote payload is in V2 (JSON) format (using a magic 139 | * value) or legacy V1 format. It reads the first two bytes, wraps them in a 140 | * ByteBuf to check the magic, and then pushes the bytes back into the stream. 141 | * 142 | * @param in the PushbackInputStream containing the vote payload. 143 | * @return VoteProtocolVersion.V2 if the magic matches PROTOCOL_2_MAGIC, 144 | * otherwise V1. 145 | * @throws IOException if there is an error reading from the stream. 146 | */ 147 | private VoteProtocolVersion checkVoteVersion(PushbackInputStream in) throws IOException { 148 | byte[] header = new byte[2]; 149 | int bytesRead = in.read(header); 150 | if (bytesRead < 2) { 151 | throw new IOException("Not enough data available to determine vote protocol version."); 152 | } 153 | 154 | if ((char) header[0] == '{') { 155 | in.unread(header, 0, bytesRead); 156 | return VoteProtocolVersion.V2; 157 | } 158 | 159 | // Wrap the header bytes into a ByteBuf for magic value checking. 160 | ByteBuf buf = Unpooled.wrappedBuffer(header); 161 | short magic = buf.getShort(0); 162 | // Push the header bytes back into the stream. 163 | in.unread(header, 0, bytesRead); 164 | 165 | if (magic == PROTOCOL_2_MAGIC) { 166 | return VoteProtocolVersion.V2; 167 | } 168 | return VoteProtocolVersion.V1; 169 | } 170 | 171 | @Override 172 | public void run() { 173 | while (running) { 174 | String address = ""; 175 | try (Socket socket = server.accept()) { 176 | address = socket.getRemoteSocketAddress().toString(); 177 | debug("Accepted connection from: " + address); 178 | socket.setSoTimeout(5000); 179 | PushbackInputStream in = new PushbackInputStream(socket.getInputStream(), 512); 180 | BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 181 | 182 | // Send handshake greeting immediately. 183 | String message = ""; 184 | if (isUseTokens()) { 185 | message = "VOTIFIER 2"; 186 | } else { 187 | message = "VOTIFIER 1"; 188 | } 189 | String challenge = getChallenge(); 190 | if (isUseTokens()) { 191 | message += " " + challenge; 192 | } 193 | // Check for pre-existing V1 vote payload before sending handshake. 194 | // Some sites may send a vote payload immediately after connecting. 195 | int avail = in.available(); 196 | 197 | if (avail >= 256) { 198 | // If there are at least 256 bytes available, assume this is a legacy V1 vote 199 | // payload. 200 | debug("Detected V1 vote payload before handshake (available bytes: " + avail 201 | + "), skipping handshake."); 202 | } else { 203 | writer.write(message); 204 | writer.newLine(); 205 | writer.flush(); 206 | debug("Sent handshake: " + message); 207 | } 208 | 209 | // Process any proxy headers if available. 210 | if (in.available() > 0) { 211 | processProxyHeaders(in, writer); 212 | } 213 | 214 | // Wait for vote payload for up to 2000ms. 215 | long waitStart = System.currentTimeMillis(); 216 | while (in.available() == 0 && System.currentTimeMillis() - waitStart < 2000) { 217 | try { 218 | Thread.sleep(50); 219 | } catch (InterruptedException ie) { 220 | Thread.currentThread().interrupt(); 221 | } 222 | } 223 | if (in.available() == 0) { 224 | debug("No vote payload received after handshake; closing connection from " + address); 225 | writer.close(); 226 | in.close(); 227 | socket.close(); 228 | continue; 229 | } 230 | 231 | // --- Determine protocol type and read vote payload --- 232 | VoteProtocolVersion voteProtocolVersion = checkVoteVersion(in); 233 | debug("Detected vote protocol version: " + voteProtocolVersion.toString()); 234 | String voteData = null; 235 | if (voteProtocolVersion.equals(VoteProtocolVersion.V1)) { 236 | byte[] block = new byte[256]; 237 | int totalRead = 0; 238 | long startTime = System.currentTimeMillis(); 239 | debug("Reading V1 vote block (256 bytes expected) at " + startTime + " ms"); 240 | 241 | if (in.available() < 256) { 242 | debug("Insufficient data available for V1 vote block; closing connection from " + address); 243 | writer.close(); 244 | in.close(); 245 | socket.close(); 246 | continue; 247 | } else { 248 | 249 | while (totalRead < block.length) { 250 | int remaining = block.length - totalRead; 251 | int r = in.read(block, totalRead, remaining); 252 | if (r == -1) { 253 | debug("Reached end-of-stream unexpectedly after " + totalRead + " bytes from " 254 | + address); 255 | break; 256 | } 257 | totalRead += r; 258 | debug("Read " + r + " bytes; total: " + totalRead); 259 | } 260 | if (totalRead == 256) { 261 | byte[] decrypted; 262 | try { 263 | decrypted = RSA.decrypt(block, getKeyPair().getPrivate()); 264 | } catch (BadPaddingException e) { 265 | StringBuilder blockHex = new StringBuilder(); 266 | for (byte b : block) { 267 | blockHex.append(String.format("%02X ", b)); 268 | } 269 | logWarning( 270 | "Decryption failed. Either the vote block is invalid or the public key does not match the server list from " 271 | + address); 272 | throw e; 273 | } 274 | int position = 0; 275 | String opcode = readString(decrypted, position); 276 | position += opcode.length() + 1; 277 | if (!opcode.equals("VOTE")) { 278 | throw new Exception("Unable to decode RSA: invalid opcode " + opcode); 279 | } 280 | String serviceName = readString(decrypted, position); 281 | position += serviceName.length() + 1; 282 | String username = readString(decrypted, position); 283 | position += username.length() + 1; 284 | String address1 = readString(decrypted, position); 285 | position += address1.length() + 1; 286 | String timeStamp = readString(decrypted, position); 287 | position += timeStamp.length() + 1; 288 | voteData = "VOTE\n" + serviceName + "\n" + username + "\n" + address1 + "\n" + timeStamp 289 | + "\n"; 290 | debug("Processed V1 vote block."); 291 | } else { 292 | debug("Failed to read V1 vote, random ping? expected 256 bytes, got " + totalRead); 293 | continue; 294 | // throw new Exception("Failed to read V1 vote block: expected 256 bytes, got " 295 | // + totalRead); 296 | } 297 | } 298 | } 299 | if (voteProtocolVersion.equals(VoteProtocolVersion.V2)) { 300 | // In V2 mode, always parse as JSON. 301 | ByteArrayOutputStream voteDataStream = new ByteArrayOutputStream(); 302 | int b; 303 | while ((b = in.read()) != -1) { 304 | voteDataStream.write(b); 305 | } 306 | voteData = voteDataStream.toString("UTF-8").trim(); 307 | debug("Received raw V2 vote payload: [" + voteData + "]"); 308 | } 309 | 310 | // --- Parse Vote Data (V2 JSON mode) --- 311 | String serviceName, username, address1, timeStamp = ""; 312 | if (voteProtocolVersion.equals(VoteProtocolVersion.V2)) { 313 | // Remove any extraneous characters before the first '{' 314 | int firstBrace = voteData.indexOf('{'); 315 | if (firstBrace > 0) { 316 | voteData = voteData.substring(firstBrace); 317 | } 318 | 319 | // Find the first '{' and the last '}' to extract JSON. 320 | int jsonStart = voteData.indexOf("{"); 321 | int jsonEnd = voteData.lastIndexOf("}"); 322 | if (jsonStart == -1 || jsonEnd == -1 || jsonStart > jsonEnd) { 323 | throw new Exception( 324 | "Expected JSON-formatted vote payload, got: " + voteData + " from " + address); 325 | } 326 | String jsonPayloadRaw = voteData.substring(jsonStart, jsonEnd + 1).trim(); 327 | debug("Extracted raw JSON payload: [" + jsonPayloadRaw + "]"); 328 | 329 | // Check if the JSON payload is an array and, if so, extract the first object. 330 | JsonObject voteMessage; 331 | if (jsonPayloadRaw.startsWith("[")) { 332 | JsonArray jsonArray = gson.fromJson(jsonPayloadRaw, JsonArray.class); 333 | if (jsonArray.size() == 0) { 334 | throw new Exception("Empty JSON array in vote payload from " + address); 335 | } 336 | voteMessage = jsonArray.get(0).getAsJsonObject(); 337 | } else { 338 | voteMessage = gson.fromJson(jsonPayloadRaw, JsonObject.class); 339 | } 340 | 341 | // Extract the inner payload and signature. 342 | String payload = voteMessage.get("payload").getAsString(); 343 | String sigHash = voteMessage.get("signature").getAsString(); 344 | byte[] sigBytes = Base64.getDecoder().decode(sigHash); 345 | 346 | // Parse the inner payload JSON. 347 | JsonObject votePayload = gson.fromJson(payload, JsonObject.class); 348 | 349 | // Retrieve serviceName from the inner JSON. 350 | String serviceNameFromPayload = votePayload.get("serviceName").getAsString(); 351 | 352 | // Lookup the token using the serviceName from the inner payload. 353 | Key key = getTokens().get(serviceNameFromPayload); 354 | if (key == null) { 355 | key = getTokens().get("default"); 356 | if (key == null) { 357 | throw new Exception("Unknown service '" + serviceNameFromPayload + "'"); 358 | } 359 | } 360 | 361 | // Debug: log the payload string and its computed HMAC for comparison. 362 | debug("Inner payload string: [" + payload + "]"); 363 | 364 | // Verify HMAC signature using the payload bytes. 365 | if (!hmacEqual(sigBytes, payload.getBytes(StandardCharsets.UTF_8), key)) { 366 | throw new Exception("Signature is not valid (invalid token?) from " + address); 367 | } 368 | 369 | // Extract vote fields from the inner payload. 370 | serviceName = serviceNameFromPayload; 371 | username = votePayload.get("username").getAsString(); 372 | address1 = votePayload.get("address").getAsString(); 373 | timeStamp = votePayload.get("timestamp").getAsString(); 374 | 375 | // Check the challenge. 376 | if (!votePayload.has("challenge")) { 377 | throw new Exception("Vote payload missing challenge field from " + address); 378 | } 379 | String receivedChallenge = votePayload.get("challenge").getAsString().trim(); 380 | if (!receivedChallenge.equals(challenge.trim())) { 381 | throw new Exception( 382 | "Invalid challenge: expected " + challenge + " but got " + receivedChallenge); 383 | } 384 | } else { 385 | String[] fields = voteData.split("\n"); 386 | serviceName = fields[1]; 387 | username = fields[2]; 388 | address1 = fields[3]; 389 | timeStamp = fields[4]; 390 | } 391 | 392 | // --- Create and Process Vote --- 393 | final Vote vote = new Vote(); 394 | vote.setServiceName(serviceName); 395 | vote.setUsername(username); 396 | vote.setAddress(address1); 397 | vote.setTimeStamp(timeStamp); 398 | if (address != null) { 399 | vote.setSourceAddress(address); 400 | } else { 401 | vote.setSourceAddress("unknown"); 402 | } 403 | if (timeStamp.equalsIgnoreCase("TestVote")) { 404 | log("Test vote received"); 405 | } 406 | log("Received vote record -> " + vote); 407 | 408 | // Send OK response. 409 | if (!timeStamp.equalsIgnoreCase("TestVote")) { 410 | try { 411 | JsonObject okResponse = new JsonObject(); 412 | okResponse.addProperty("status", "ok"); 413 | String okMessage = gson.toJson(okResponse) + "\r\n"; 414 | writer.write(okMessage); 415 | writer.flush(); 416 | debug("Sent OK response: " + okMessage); 417 | } catch (Exception e) { 418 | debug("Failed to send OK response, but will continue to process vote: " 419 | + e.getLocalizedMessage()); 420 | } 421 | } 422 | 423 | // --- Forward Vote to Other Servers --- 424 | for (String server : getServers()) { 425 | ForwardServer forwardServer = getServerData(server); 426 | if (forwardServer.isEnabled()) { 427 | debug("Forwarding vote to: " + server); 428 | String voteString = "VOTE\n" + vote.getServiceName() + "\n" + vote.getUsername() + "\n" 429 | + vote.getAddress() + "\n" + vote.getTimeStamp() + "\n"; 430 | try { 431 | SocketAddress sockAddr = new InetSocketAddress(forwardServer.getHost(), 432 | forwardServer.getPort()); 433 | try (Socket forwardSocket = new Socket()) { 434 | forwardSocket.connect(sockAddr, 1000); 435 | OutputStream outStream = forwardSocket.getOutputStream(); 436 | 437 | byte[] encrypted = encrypt(voteString.getBytes(StandardCharsets.UTF_8), 438 | getPublicKey(forwardServer)); 439 | outStream.write(encrypted); 440 | 441 | outStream.flush(); 442 | } 443 | } catch (Exception e) { 444 | log("Failed to forward vote to " + server + " (" + forwardServer.getHost() + ":" 445 | + forwardServer.getPort() + "): " + vote.toString()); 446 | debug(e); 447 | } 448 | } 449 | } 450 | callEvent(vote); 451 | writer.close(); 452 | in.close(); 453 | socket.close(); 454 | } catch (MalformedJsonException ex) { 455 | logWarning("Malformed JSON payload received from: " + address + " - " + ex.getMessage()); 456 | debug(ex); 457 | } catch (SocketException ex) { 458 | if (running) { 459 | logWarning("Protocol error from: " + address + " - " + ex.getLocalizedMessage()); 460 | debug(ex); 461 | } else { 462 | logWarning("Votifier socket closed."); 463 | } 464 | } catch (BadPaddingException ex) { 465 | logWarning("Unable to decrypt vote record from: " + address 466 | + ". Make sure that your public key matches the one you gave the server list."); 467 | debug(ex); 468 | } catch (SocketTimeoutException ex) { 469 | logWarning("Socket timeout while waiting for vote payload from: " + address + " - " + ex.getMessage()); 470 | debug(ex); 471 | } catch (Exception ex) { 472 | logWarning("Exception caught while receiving a vote notification from: " + address + " - " 473 | + ex.getLocalizedMessage()); 474 | debug(ex); 475 | } 476 | } 477 | } 478 | 479 | private String readString(byte[] data, int offset) { 480 | StringBuilder builder = new StringBuilder(); 481 | for (int i = offset; i < data.length; i++) { 482 | if (data[i] == '\n') 483 | break; 484 | builder.append((char) data[i]); 485 | } 486 | return builder.toString(); 487 | } 488 | 489 | private String readLine(PushbackInputStream in) throws Exception { 490 | ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(); 491 | int b; 492 | boolean seenCR = false; 493 | while ((b = in.read()) != -1) { 494 | if (b == '\r') { 495 | seenCR = true; 496 | continue; 497 | } 498 | if (b == '\n') { 499 | break; 500 | } 501 | if (seenCR) { 502 | in.unread(b); 503 | break; 504 | } 505 | lineBuffer.write(b); 506 | } 507 | return lineBuffer.toString("ASCII").trim(); 508 | } 509 | 510 | private boolean isProxyV2(byte[] header) { 511 | for (int i = 0; i < PROXY_V2_SIGNATURE.length; i++) { 512 | if (header[i] != PROXY_V2_SIGNATURE[i]) { 513 | return false; 514 | } 515 | } 516 | return true; 517 | } 518 | 519 | public abstract void logWarning(String warn); 520 | 521 | public abstract void logSevere(String msg); 522 | 523 | public abstract void log(String msg); 524 | 525 | public abstract void debug(String msg); 526 | 527 | public abstract String getVersion(); 528 | 529 | public abstract Set getServers(); 530 | 531 | public abstract KeyPair getKeyPair(); 532 | 533 | public abstract Map getTokens(); 534 | 535 | public abstract ForwardServer getServerData(String s); 536 | 537 | public abstract void callEvent(Vote e); 538 | 539 | public abstract void debug(Exception e); 540 | 541 | public byte[] encrypt(byte[] data, PublicKey key) throws Exception { 542 | Cipher cipher = Cipher.getInstance("RSA"); 543 | cipher.init(Cipher.ENCRYPT_MODE, key); 544 | return cipher.doFinal(data); 545 | } 546 | 547 | public PublicKey getPublicKey(ForwardServer forwardServer) throws Exception { 548 | byte[] encoded = Base64.getDecoder().decode(forwardServer.getKey()); 549 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 550 | return keyFactory.generatePublic(new X509EncodedKeySpec(encoded)); 551 | } 552 | 553 | // Generates a challenge string using TokenUtil. 554 | public String getChallenge() { 555 | return TokenUtil.newToken(); 556 | } 557 | 558 | /** 559 | * Processes and discards any proxy header data if present. 560 | */ 561 | private void processProxyHeaders(PushbackInputStream in, BufferedWriter writer) throws Exception { 562 | byte[] headerPeek = new byte[32]; 563 | int bytesRead = in.read(headerPeek); 564 | if (bytesRead > 0) { 565 | String headerString = new String(headerPeek, 0, bytesRead, StandardCharsets.US_ASCII); 566 | if (headerString.startsWith("PROXY") && !headerString.contains("CONNECT")) { 567 | in.unread(headerPeek, 0, bytesRead); 568 | ByteArrayOutputStream headerLine = new ByteArrayOutputStream(); 569 | byte[] buf = new byte[1]; 570 | while (in.read(buf) != -1) { 571 | headerLine.write(buf[0]); 572 | if (buf[0] == '\n') 573 | break; 574 | } 575 | String proxyHeader = headerLine.toString("ASCII").trim(); 576 | debug("Discarded PROXY (v1) header: " + proxyHeader); 577 | } else if (bytesRead >= 12 && isProxyV2(headerPeek)) { 578 | int addrLength = ((headerPeek[14] & 0xFF) << 8) | (headerPeek[15] & 0xFF); 579 | int totalV2HeaderLength = 16 + addrLength; 580 | int remaining = totalV2HeaderLength - bytesRead; 581 | byte[] discard = new byte[remaining]; 582 | int readRemaining = 0; 583 | while (readRemaining < remaining) { 584 | int r = in.read(discard, readRemaining, remaining - readRemaining); 585 | if (r == -1) 586 | break; 587 | readRemaining += r; 588 | } 589 | if (readRemaining != remaining) { 590 | throw new Exception("Incomplete PROXY protocol v2 header"); 591 | } 592 | debug("Discarded PROXY protocol v2 header (" + totalV2HeaderLength + " bytes)"); 593 | } else if (headerString.startsWith("CONNECT")) { 594 | in.unread(headerPeek, 0, bytesRead); 595 | String connectLine = readLine(in); 596 | debug("Received CONNECT request: " + connectLine); 597 | String line; 598 | while (!(line = readLine(in)).isEmpty()) { 599 | debug("Discarding header: " + line); 600 | } 601 | writer.write("HTTP/1.1 200 Connection Established\r\n\r\n"); 602 | writer.flush(); 603 | } else { 604 | in.unread(headerPeek, 0, bytesRead); 605 | } 606 | } 607 | } 608 | 609 | /** 610 | * Compares the provided HMAC signature with a computed HMAC of the data. 611 | */ 612 | private boolean hmacEqual(byte[] providedSig, byte[] data, Key key) throws Exception { 613 | Mac mac = Mac.getInstance("HmacSHA256"); 614 | mac.init(new SecretKeySpec(key.getEncoded(), "HmacSHA256")); 615 | byte[] computed = mac.doFinal(data); 616 | if (providedSig.length != computed.length) { 617 | return false; 618 | } 619 | for (int i = 0; i < providedSig.length; i++) { 620 | if (providedSig[i] != computed[i]) { 621 | return false; 622 | } 623 | } 624 | return true; 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/Config.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.velocity; 2 | 3 | import java.io.File; 4 | import java.util.Collection; 5 | 6 | import org.checkerframework.checker.nullness.qual.NonNull; 7 | 8 | import com.bencodez.simpleapi.file.velocity.VelocityYMLFile; 9 | 10 | import ninja.leaping.configurate.ConfigurationNode; 11 | 12 | public class Config extends VelocityYMLFile { 13 | 14 | public Config(File file) { 15 | super(file); 16 | } 17 | 18 | public String getHost() { 19 | return getString(getNode("host"), ""); 20 | } 21 | 22 | public int getPort() { 23 | return getInt(getNode("port"), 0); 24 | } 25 | 26 | public boolean getDebug() { 27 | return getBoolean(getNode("Debug"), false); 28 | } 29 | 30 | public @NonNull Collection getServers() { 31 | return getNode("Forwarding").getChildrenMap().values(); 32 | } 33 | 34 | public ConfigurationNode getServersData(String s) { 35 | return getNode("Forwarding", s); 36 | } 37 | 38 | public @NonNull Collection getTokens() { 39 | return getNode("tokens").getChildrenMap().values(); 40 | } 41 | 42 | public String getToken(String key) { 43 | return getString(getNode("tokens", key), null); 44 | } 45 | 46 | public boolean containsTokens() { 47 | return getNode("tokens").getValue() != null; 48 | } 49 | 50 | public void setToken(String key, String token) { 51 | getNode("tokens", key).setValue(token); 52 | save(); 53 | } 54 | 55 | public boolean getTokenSupport() { 56 | return getBoolean(getNode("TokenSupport"), false); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocity.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.velocity; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.Reader; 10 | import java.net.URL; 11 | import java.nio.file.Path; 12 | import java.security.CodeSource; 13 | import java.security.Key; 14 | import java.security.KeyPair; 15 | import java.util.HashMap; 16 | import java.util.HashSet; 17 | import java.util.Map; 18 | import java.util.Set; 19 | import java.util.zip.ZipEntry; 20 | import java.util.zip.ZipInputStream; 21 | 22 | import org.bstats.charts.SimplePie; 23 | import org.bstats.velocity.Metrics; 24 | import org.slf4j.Logger; 25 | 26 | import com.google.inject.Inject; 27 | import com.velocitypowered.api.command.CommandMeta; 28 | import com.velocitypowered.api.event.Subscribe; 29 | import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; 30 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; 31 | import com.velocitypowered.api.plugin.Plugin; 32 | import com.velocitypowered.api.plugin.annotation.DataDirectory; 33 | import com.velocitypowered.api.proxy.ProxyServer; 34 | import com.vexsoftware.votifier.ForwardServer; 35 | import com.vexsoftware.votifier.crypto.RSAIO; 36 | import com.vexsoftware.votifier.crypto.RSAKeygen; 37 | import com.vexsoftware.votifier.crypto.TokenUtil; 38 | import com.vexsoftware.votifier.model.Vote; 39 | import com.vexsoftware.votifier.net.VoteReceiver; 40 | 41 | import lombok.Getter; 42 | import lombok.Setter; 43 | import ninja.leaping.configurate.ConfigurationNode; 44 | import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; 45 | 46 | @Plugin(id = "votifierplus", name = "VotifierPlus", version = "1.0", url = "https://www.spigotmc.org/resources/votifierplus.74040", description = "Votifier Velocity Version", authors = { 47 | "BenCodez" }) 48 | public class VotifierPlusVelocity { 49 | @Getter 50 | private VoteReceiver voteReceiver; 51 | @Getter 52 | private Config config; 53 | @Getter 54 | @Setter 55 | private KeyPair keyPair; 56 | private ProxyServer server; 57 | private Logger logger; 58 | @Getter 59 | private Path dataDirectory; 60 | private final Metrics.Factory metricsFactory; 61 | private Object buildNumber = "NOTSET"; 62 | private String version; 63 | private File versionFile; 64 | 65 | @Inject 66 | public VotifierPlusVelocity(ProxyServer server, Logger logger, Metrics.Factory metricsFactory, 67 | @DataDirectory Path dataDirectory) { 68 | this.server = server; 69 | this.logger = logger; 70 | this.dataDirectory = dataDirectory; 71 | this.metricsFactory = metricsFactory; 72 | } 73 | 74 | @Subscribe 75 | public void onProxyDisable(ProxyShutdownEvent event) { 76 | voteReceiver.shutdown(); 77 | } 78 | 79 | private HashMap tokens = new HashMap(); 80 | 81 | private void loadTokens() { 82 | tokens.clear(); 83 | if (!config.containsTokens()) { 84 | config.setToken("default", TokenUtil.newToken()); 85 | } 86 | 87 | for (ConfigurationNode key : config.getTokens()) { 88 | tokens.put(key.getKey().toString(), TokenUtil.createKeyFrom(config.getToken(key.getKey().toString()))); 89 | } 90 | } 91 | 92 | private void getVersionFile() { 93 | try { 94 | CodeSource src = this.getClass().getProtectionDomain().getCodeSource(); 95 | if (src != null) { 96 | URL jar = src.getLocation(); 97 | ZipInputStream zip = null; 98 | zip = new ZipInputStream(jar.openStream()); 99 | while (true) { 100 | ZipEntry e = zip.getNextEntry(); 101 | if (e != null) { 102 | String name = e.getName(); 103 | if (name.equals("votifierplusversion.yml")) { 104 | Reader defConfigStream = new InputStreamReader(zip); 105 | if (defConfigStream != null) { 106 | versionFile = new File(dataDirectory.toFile(), 107 | "tmp" + File.separator + "votifierplusversion.yml"); 108 | if (!versionFile.exists()) { 109 | versionFile.getParentFile().mkdirs(); 110 | versionFile.createNewFile(); 111 | } 112 | FileWriter fileWriter = new FileWriter(versionFile); 113 | 114 | int charVal; 115 | while ((charVal = defConfigStream.read()) != -1) { 116 | fileWriter.append((char) charVal); 117 | } 118 | 119 | fileWriter.close(); 120 | YAMLConfigurationLoader loader = YAMLConfigurationLoader.builder().setFile(versionFile) 121 | .build(); 122 | defConfigStream.close(); 123 | ConfigurationNode node = loader.load(); 124 | if (node != null) { 125 | version = node.getNode("version").getString(""); 126 | buildNumber = node.getNode("buildnumber").getString("NOTSET"); 127 | } 128 | return; 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } catch (Exception e) { 135 | e.printStackTrace(); 136 | } 137 | } 138 | 139 | @Subscribe 140 | public void onProxyInitialization(ProxyInitializeEvent event) { 141 | File configFile = new File(dataDirectory.toFile(), "bungeeconfig.yml"); 142 | configFile.getParentFile().mkdirs(); 143 | if (!configFile.exists()) { 144 | try { 145 | configFile.createNewFile(); 146 | } catch (IOException e) { 147 | e.printStackTrace(); 148 | } 149 | 150 | InputStream toCopyStream = VotifierPlusVelocity.class.getClassLoader() 151 | .getResourceAsStream("bungeeconfig.yml"); 152 | 153 | try (FileOutputStream fos = new FileOutputStream(configFile)) { 154 | byte[] buf = new byte[2048]; 155 | int r; 156 | while (-1 != (r = toCopyStream.read(buf))) { 157 | fos.write(buf, 0, r); 158 | } 159 | } catch (IOException e) { 160 | e.printStackTrace(); 161 | } 162 | } 163 | config = new Config(configFile); 164 | 165 | loadTokens(); 166 | 167 | CommandMeta meta = server.getCommandManager().metaBuilder("votifierplusbungee") 168 | // Specify other aliases (optional) 169 | .aliases("votifierplus", "votifierplusvelocity").build(); 170 | server.getCommandManager().register(meta, new VotifierPlusVelocityCommand(this)); 171 | loadVoteReceiver(); 172 | File rsaDirectory = new File(dataDirectory.toFile(), "rsa"); 173 | /* 174 | * Create RSA directory and keys if it does not exist; otherwise, read keys. 175 | */ 176 | try { 177 | if (!rsaDirectory.exists()) { 178 | rsaDirectory.mkdir(); 179 | keyPair = RSAKeygen.generate(2048); 180 | RSAIO.save(rsaDirectory, keyPair); 181 | } else { 182 | keyPair = RSAIO.load(rsaDirectory); 183 | } 184 | } catch (Exception ex) { 185 | logger.error("Error reading configuration file or RSA keys"); 186 | return; 187 | } 188 | 189 | try { 190 | getVersionFile(); 191 | if (versionFile != null) { 192 | versionFile.delete(); 193 | versionFile.getParentFile().delete(); 194 | } 195 | } catch (Exception e) { 196 | e.printStackTrace(); 197 | } 198 | Metrics metrics = metricsFactory.make(this, 20282); 199 | metrics.addCustomChart(new SimplePie("plugin_version", () -> "" + version)); 200 | if (!buildNumber.equals("NOTSET")) { 201 | metrics.addCustomChart(new SimplePie("dev_build_number", () -> "" + buildNumber)); 202 | } 203 | 204 | logger.info("VotingPlugin velocity loaded, " + "Internal Jar Version: " + version); 205 | if (!buildNumber.equals("NOTSET")) { 206 | logger.info("Detected using dev build number: " + buildNumber); 207 | } 208 | 209 | } 210 | 211 | private void loadVoteReceiver() { 212 | try { 213 | voteReceiver = new VoteReceiver(config.getHost(), config.getPort()) { 214 | 215 | @Override 216 | public void logWarning(String warn) { 217 | logger.warn(warn); 218 | } 219 | 220 | @Override 221 | public void logSevere(String msg) { 222 | logger.error(msg); 223 | } 224 | 225 | @Override 226 | public void log(String msg) { 227 | logger.info(msg); 228 | } 229 | 230 | @Override 231 | public String getVersion() { 232 | return version; 233 | } 234 | 235 | @Override 236 | public Set getServers() { 237 | Set servers = new HashSet(); 238 | for (ConfigurationNode node : config.getServers()) { 239 | servers.add(node.getKey().toString()); 240 | } 241 | return servers; 242 | } 243 | 244 | @Override 245 | public ForwardServer getServerData(String s) { 246 | ConfigurationNode d = config.getServersData(s); 247 | return new ForwardServer(d.getNode("Enabled").getBoolean(), d.getNode("Host").getString(), 248 | d.getNode("Port").getInt(), d.getNode("Key").getString()); 249 | } 250 | 251 | @Override 252 | public KeyPair getKeyPair() { 253 | return keyPair; 254 | } 255 | 256 | @Override 257 | public void debug(Exception e) { 258 | if (config.getDebug()) { 259 | e.printStackTrace(); 260 | } 261 | } 262 | 263 | @Override 264 | public void debug(String debug) { 265 | if (config.getDebug()) { 266 | logger.info("Debug: " + debug); 267 | } 268 | } 269 | 270 | @Override 271 | public void callEvent(Vote vote) { 272 | server.getEventManager().fire(new com.vexsoftware.votifier.velocity.event.VotifierEvent(vote)); 273 | } 274 | 275 | @Override 276 | public Map getTokens() { 277 | return tokens; 278 | } 279 | 280 | @Override 281 | public boolean isUseTokens() { 282 | return config.getTokenSupport(); 283 | } 284 | }; 285 | voteReceiver.start(); 286 | 287 | logger.info("Votifier enabled."); 288 | } catch (Exception ex) { 289 | return; 290 | } 291 | } 292 | 293 | public void reload() { 294 | config.reload(); 295 | loadTokens(); 296 | loadVoteReceiver(); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/VotifierPlusVelocityCommand.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.velocity; 2 | 3 | import java.io.File; 4 | import java.io.OutputStream; 5 | import java.net.InetSocketAddress; 6 | import java.net.Socket; 7 | import java.net.SocketAddress; 8 | import java.security.PublicKey; 9 | 10 | import com.velocitypowered.api.command.CommandSource; 11 | import com.velocitypowered.api.command.SimpleCommand; 12 | import com.vexsoftware.votifier.crypto.RSAIO; 13 | import com.vexsoftware.votifier.crypto.RSAKeygen; 14 | 15 | import net.kyori.adventure.text.Component; 16 | import net.kyori.adventure.text.format.NamedTextColor; 17 | 18 | public class VotifierPlusVelocityCommand implements SimpleCommand { 19 | private VotifierPlusVelocity plugin; 20 | 21 | public VotifierPlusVelocityCommand(VotifierPlusVelocity plugin) { 22 | this.plugin = plugin; 23 | } 24 | 25 | @Override 26 | public void execute(final Invocation invocation) { 27 | CommandSource source = invocation.source(); 28 | // Get the arguments after the command alias 29 | String[] args = invocation.arguments(); 30 | 31 | if (hasPermission(invocation)) { 32 | if (args.length > 0) { 33 | if (args[0].equalsIgnoreCase("reload")) { 34 | plugin.reload(); 35 | source.sendMessage(Component.text("Reloading VotifierPlus").color(NamedTextColor.AQUA)); 36 | } 37 | if (args[0].equalsIgnoreCase("GenerateKeys")) { 38 | File rsaDirectory = new File(plugin.getDataDirectory() + File.separator + "rsa"); 39 | 40 | try { 41 | for (File file : rsaDirectory.listFiles()) { 42 | if (!file.isDirectory()) { 43 | file.delete(); 44 | } 45 | } 46 | rsaDirectory.mkdir(); 47 | plugin.setKeyPair(RSAKeygen.generate(2048)); 48 | RSAIO.save(rsaDirectory, plugin.getKeyPair()); 49 | } catch (Exception ex) { 50 | source.sendMessage(Component.text("Failed to create keys")); 51 | return; 52 | } 53 | source.sendMessage(Component.text("New keys generated")); 54 | } 55 | if (args[0].equalsIgnoreCase("vote") && args.length > 2) { 56 | try { 57 | PublicKey publicKey = plugin.getKeyPair().getPublic(); 58 | String serverIP = plugin.getConfig().getHost(); 59 | int serverPort = plugin.getConfig().getPort(); 60 | 61 | String VoteString = "VOTE\n" + args[2] + "\n" + args[1] + "\n" + "Address" + "\n" + "TestVote" 62 | + "\n"; 63 | 64 | SocketAddress sockAddr = new InetSocketAddress(serverIP, serverPort); 65 | Socket socket1 = new Socket(); 66 | socket1.connect(sockAddr, 1000); 67 | OutputStream socketOutputStream = socket1.getOutputStream(); 68 | socketOutputStream.write(plugin.getVoteReceiver().encrypt(VoteString.getBytes(), publicKey)); 69 | socketOutputStream.close(); 70 | socket1.close(); 71 | source.sendMessage(Component.text("Vote triggered")); 72 | 73 | } catch (Exception e) { 74 | e.printStackTrace(); 75 | 76 | } 77 | 78 | } 79 | 80 | } 81 | } else { 82 | source.sendMessage(Component.text("You do not have permission to do this!")); 83 | } 84 | } 85 | 86 | @Override 87 | public boolean hasPermission(final Invocation invocation) { 88 | return invocation.source().hasPermission("votifierplus.admin"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/java/com/vexsoftware/votifier/velocity/event/VotifierEvent.java: -------------------------------------------------------------------------------- 1 | package com.vexsoftware.votifier.velocity.event; 2 | 3 | import com.velocitypowered.api.event.ResultedEvent; 4 | import com.vexsoftware.votifier.model.Vote; 5 | 6 | public class VotifierEvent implements ResultedEvent { 7 | private final Vote vote; 8 | private GenericResult result; 9 | 10 | public VotifierEvent(Vote vote) { 11 | this.vote = vote; 12 | this.result = GenericResult.allowed(); 13 | } 14 | 15 | public Vote getVote() { 16 | return vote; 17 | } 18 | 19 | @Override 20 | public GenericResult getResult() { 21 | return this.result; 22 | } 23 | 24 | @Override 25 | public void setResult(GenericResult result) { 26 | this.result = result; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "VotifierEvent{" + "vote=" + vote + '}'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /VotifierPlus/src/main/resources/bungee.yml: -------------------------------------------------------------------------------- 1 | name: ${project.name} 2 | version: ${project.version} 3 | main: com.vexsoftware.votifier.bungee.VotifierPlusBungee 4 | author: BenCodez -------------------------------------------------------------------------------- /VotifierPlus/src/main/resources/bungeeconfig.yml: -------------------------------------------------------------------------------- 1 | Debug: false 2 | # The host VotifierPlus will listen on 3 | host: 0.0.0.0 4 | # The port VotifierPlus will listen on 5 | port: 8192 6 | # This is still new to VotifierPlus, so it's disabled by default. 7 | TokenSupport: false 8 | # If your using VotingPlugin you don't need this 9 | # Doesn't support tokens yet 10 | Forwarding: 11 | server1: 12 | Enabled: false 13 | Host: '' 14 | Port: '' 15 | Key: '' -------------------------------------------------------------------------------- /VotifierPlus/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # Debug levels: 2 | # NONE 3 | # INFO 4 | # EXTRA 5 | DebugLevel: NONE 6 | # The host VotifierPlus will listen on 7 | host: 0.0.0.0 8 | # The port VotifierPlus will listen on 9 | port: 8192 10 | # This is still new to VotifierPlus, so it's disabled by default. 11 | TokenSupport: false 12 | # If your using VotingPlugin you don't need this 13 | # Doesn't support tokens yet 14 | Forwarding: 15 | server1: 16 | Enabled: false 17 | Host: '' 18 | Port: 8193 19 | Key: '' -------------------------------------------------------------------------------- /VotifierPlus/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ${project.name} 2 | main: com.vexsoftware.votifier.VotifierPlus 3 | version: "${project.version}" 4 | description: A plugin that gets notified when votes are made for the server on toplists. 5 | author: BenCodez 6 | api-version: 1.13 7 | softdepend: [SuperVanish, PremiumVanish] 8 | folia-supported: true 9 | commands: 10 | votifierplus: 11 | description: main command -------------------------------------------------------------------------------- /VotifierPlus/src/main/resources/votifierplusversion.yml: -------------------------------------------------------------------------------- 1 | time: '${timestamp}' 2 | profile: '${build.profile.id}' 3 | version: '${project.version}' 4 | buildnumber: '${build.number}' -------------------------------------------------------------------------------- /VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/RSATest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.bencodez.votifierplus.tests; 3 | 4 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import java.security.KeyPair; 9 | import java.security.KeyPairGenerator; 10 | import java.security.PrivateKey; 11 | import java.security.PublicKey; 12 | import java.security.InvalidKeyException; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | import com.vexsoftware.votifier.crypto.RSA; 17 | import com.vexsoftware.votifier.crypto.RSAKeygen; 18 | 19 | public class RSATest { 20 | 21 | @Test 22 | public void encryptDecryptWithValidKeys() throws Exception { 23 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); 24 | keyGen.initialize(2048); 25 | KeyPair keyPair = keyGen.generateKeyPair(); 26 | PublicKey publicKey = keyPair.getPublic(); 27 | PrivateKey privateKey = keyPair.getPrivate(); 28 | 29 | byte[] data = "Test data".getBytes(); 30 | byte[] encryptedData = RSA.encrypt(data, publicKey); 31 | byte[] decryptedData = RSA.decrypt(encryptedData, privateKey); 32 | 33 | assertArrayEquals(data, decryptedData); 34 | } 35 | 36 | @Test 37 | public void encryptWithNullDataThrowsException() throws Exception { 38 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); 39 | keyGen.initialize(2048); 40 | KeyPair keyPair = keyGen.generateKeyPair(); 41 | PublicKey publicKey = keyPair.getPublic(); 42 | 43 | assertThrows(IllegalArgumentException.class, () -> { 44 | RSA.encrypt(null, publicKey); 45 | }); 46 | } 47 | 48 | @Test 49 | public void decryptWithNullDataThrowsException() throws Exception { 50 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); 51 | keyGen.initialize(2048); 52 | KeyPair keyPair = keyGen.generateKeyPair(); 53 | PrivateKey privateKey = keyPair.getPrivate(); 54 | 55 | assertThrows(IllegalArgumentException.class, () -> { 56 | RSA.decrypt(null, privateKey); 57 | }); 58 | } 59 | 60 | @Test 61 | public void encryptWithNullKeyThrowsException() throws Exception { 62 | byte[] data = "Test data".getBytes(); 63 | 64 | assertThrows(InvalidKeyException.class, () -> { 65 | RSA.encrypt(data, null); 66 | }); 67 | } 68 | 69 | @Test 70 | public void decryptWithNullKeyThrowsException() throws Exception { 71 | byte[] data = "Test data".getBytes(); 72 | 73 | assertThrows(InvalidKeyException.class, () -> { 74 | RSA.decrypt(data, null); 75 | }); 76 | } 77 | 78 | @Test 79 | public void decryptWithIncorrectKeyThrowsException() throws Exception { 80 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); 81 | keyGen.initialize(2048); 82 | KeyPair keyPair1 = keyGen.generateKeyPair(); 83 | KeyPair keyPair2 = keyGen.generateKeyPair(); 84 | PublicKey publicKey = keyPair1.getPublic(); 85 | PrivateKey privateKey = keyPair2.getPrivate(); 86 | 87 | byte[] data = "Test data".getBytes(); 88 | byte[] encryptedData = RSA.encrypt(data, publicKey); 89 | 90 | assertThrows(Exception.class, () -> { 91 | RSA.decrypt(encryptedData, privateKey); 92 | }); 93 | } 94 | 95 | @Test 96 | public void generateKeyPairWith1024Bits() throws Exception { 97 | KeyPair keyPair = RSAKeygen.generate(1024); 98 | assertNotNull(keyPair); 99 | assertNotNull(keyPair.getPrivate()); 100 | assertNotNull(keyPair.getPublic()); 101 | } 102 | 103 | @Test 104 | public void generateKeyPairWith2048Bits() throws Exception { 105 | KeyPair keyPair = RSAKeygen.generate(2048); 106 | assertNotNull(keyPair); 107 | assertNotNull(keyPair.getPrivate()); 108 | assertNotNull(keyPair.getPublic()); 109 | } 110 | 111 | @Test 112 | public void generateKeyPairWithZeroBitsThrowsException() { 113 | assertThrows(Exception.class, () -> { 114 | RSAKeygen.generate(0); 115 | }); 116 | } 117 | 118 | @Test 119 | public void generateKeyPairWithNegativeBitsThrowsException() { 120 | assertThrows(Exception.class, () -> { 121 | RSAKeygen.generate(-1024); 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/TokenUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.bencodez.votifierplus.tests; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotNull; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import java.security.Key; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.vexsoftware.votifier.crypto.TokenUtil; 14 | 15 | public class TokenUtilTest { 16 | 17 | @Test 18 | public void newTokenGeneratesNonEmptyString() { 19 | String token = TokenUtil.newToken(); 20 | assertNotNull(token); 21 | assertFalse(token.isEmpty()); 22 | } 23 | 24 | @Test 25 | public void newTokenGeneratesUniqueTokens() { 26 | String token1 = TokenUtil.newToken(); 27 | String token2 = TokenUtil.newToken(); 28 | assertNotEquals(token1, token2); 29 | } 30 | 31 | @Test 32 | public void createKeyFromValidToken() { 33 | String token = "testToken"; 34 | Key key = TokenUtil.createKeyFrom(token); 35 | assertNotNull(key); 36 | assertEquals("HmacSHA256", key.getAlgorithm()); 37 | } 38 | 39 | @Test 40 | public void createKeyFromEmptyTokenThrowsException() { 41 | String token = ""; 42 | assertThrows(IllegalArgumentException.class, () -> { 43 | TokenUtil.createKeyFrom(token); 44 | }); 45 | } 46 | 47 | @Test 48 | public void createKeyFromNullTokenThrowsException() { 49 | assertThrows(NullPointerException.class, () -> { 50 | TokenUtil.createKeyFrom(null); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VoteReceiverTest.java: -------------------------------------------------------------------------------- 1 | package com.bencodez.votifierplus.tests; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.io.BufferedWriter; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.OutputStreamWriter; 11 | import java.io.PushbackInputStream; 12 | import java.lang.reflect.Method; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.Key; 15 | import java.security.KeyPair; 16 | import java.security.KeyPairGenerator; 17 | import java.util.Base64; 18 | import java.util.Collections; 19 | import java.util.Map; 20 | import java.util.Set; 21 | 22 | import javax.crypto.Cipher; 23 | import javax.crypto.Mac; 24 | import javax.crypto.spec.SecretKeySpec; 25 | 26 | import org.junit.jupiter.api.AfterEach; 27 | import org.junit.jupiter.api.BeforeAll; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import com.google.gson.Gson; 32 | import com.google.gson.JsonObject; 33 | import com.vexsoftware.votifier.ForwardServer; 34 | import com.vexsoftware.votifier.crypto.RSA; 35 | import com.vexsoftware.votifier.model.Vote; 36 | import com.vexsoftware.votifier.net.VoteReceiver; 37 | 38 | /** 39 | * Unit tests for processing V1 (RSA) and V2 (token/JSON) vote payloads, 40 | * including verification of the challenge and proxy header processing. 41 | */ 42 | public class VoteReceiverTest { 43 | 44 | // Test RSA key pair for v1 tests. 45 | private static KeyPair testKeyPair; 46 | // Dummy token key for v2 tests. 47 | private static Key dummyTokenKey; 48 | 49 | // Our test receiver instance; will bind to an ephemeral port (0). 50 | private TestVoteReceiver receiver; 51 | 52 | @BeforeAll 53 | public static void setupClass() throws Exception { 54 | KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); 55 | kpg.initialize(2048); 56 | testKeyPair = kpg.generateKeyPair(); 57 | // Create a dummy HMAC key (for example purposes) 58 | dummyTokenKey = new SecretKeySpec("dummySecretKey1234".getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 59 | } 60 | 61 | @BeforeEach 62 | public void setup() throws Exception { 63 | // Bind to port 0 to let the OS assign an available port. 64 | receiver = new TestVoteReceiver("127.0.0.1", 0, testKeyPair); 65 | } 66 | 67 | @AfterEach 68 | public void tearDown() { 69 | receiver.shutdown(); 70 | } 71 | 72 | /** 73 | * A dummy subclass of VoteReceiver for testing. We override abstract methods 74 | * and expose helper methods for processing votes. 75 | */ 76 | private static class TestVoteReceiver extends VoteReceiver { 77 | 78 | private final String testChallenge = "testChallenge"; 79 | 80 | public TestVoteReceiver(String host, int port, KeyPair keyPair) throws Exception { 81 | super(host, port); 82 | } 83 | 84 | /** 85 | * Process a V1 vote block. The block is assumed to be exactly the RSA-encrypted 86 | * vote block. 87 | */ 88 | public Vote processV1Vote(byte[] encryptedBlock) throws Exception { 89 | byte[] decrypted = RSA.decrypt(encryptedBlock, getKeyPair().getPrivate()); 90 | int position = 0; 91 | String opcode = readString(decrypted, position); 92 | position += opcode.length() + 1; 93 | if (!opcode.equals("VOTE")) { 94 | throw new Exception("Invalid opcode: " + opcode); 95 | } 96 | String serviceName = readString(decrypted, position); 97 | position += serviceName.length() + 1; 98 | String username = readString(decrypted, position); 99 | position += username.length() + 1; 100 | String address = readString(decrypted, position); 101 | position += address.length() + 1; 102 | String timeStamp = readString(decrypted, position); 103 | position += timeStamp.length() + 1; 104 | Vote vote = new Vote(); 105 | vote.setServiceName(serviceName); 106 | vote.setUsername(username); 107 | vote.setAddress(address); 108 | vote.setTimeStamp(timeStamp); 109 | return vote; 110 | } 111 | 112 | /** 113 | * Process a V2 vote payload in JSON format. 114 | */ 115 | public Vote processV2Vote(String jsonPayload) throws Exception { 116 | Gson gson = new Gson(); 117 | JsonObject outer = gson.fromJson(jsonPayload, JsonObject.class); 118 | String payload = outer.get("payload").getAsString(); 119 | JsonObject inner = gson.fromJson(payload, JsonObject.class); 120 | // Verify challenge. 121 | if (!inner.has("challenge")) { 122 | throw new Exception("Vote payload missing challenge field."); 123 | } 124 | String receivedChallenge = inner.get("challenge").getAsString(); 125 | if (!receivedChallenge.equals(getChallenge())) { 126 | throw new Exception("Invalid challenge: expected " + getChallenge() + " but got " + receivedChallenge); 127 | } 128 | Vote vote = new Vote(); 129 | vote.setServiceName(inner.get("serviceName").getAsString()); 130 | vote.setUsername(inner.get("username").getAsString()); 131 | vote.setAddress(inner.get("address").getAsString()); 132 | vote.setTimeStamp(inner.get("timestamp").getAsString()); 133 | 134 | return vote; 135 | } 136 | 137 | // Dummy implementations for abstract methods: 138 | @Override 139 | public boolean isUseTokens() { 140 | // For testing, we decide based on our mode. 141 | return false; 142 | } 143 | 144 | @Override 145 | public void logWarning(String warn) { 146 | } 147 | 148 | @Override 149 | public void logSevere(String msg) { 150 | } 151 | 152 | @Override 153 | public void log(String msg) { 154 | } 155 | 156 | @Override 157 | public void debug(String msg) { 158 | } 159 | 160 | @Override 161 | public String getVersion() { 162 | return "Test"; 163 | } 164 | 165 | @Override 166 | public Set getServers() { 167 | return Collections.emptySet(); 168 | } 169 | 170 | @Override 171 | public KeyPair getKeyPair() { 172 | return testKeyPair; 173 | } 174 | 175 | @Override 176 | public Map getTokens() { 177 | return Collections.singletonMap("votifier.bencodez.com", dummyTokenKey); 178 | } 179 | 180 | @Override 181 | public ForwardServer getServerData(String s) { 182 | return null; 183 | } 184 | 185 | @Override 186 | public void callEvent(Vote e) { 187 | } 188 | 189 | @Override 190 | public void debug(Exception e) { 191 | } 192 | 193 | // Expose readString method (reimplementation) 194 | public String readString(byte[] data, int offset) { 195 | StringBuilder builder = new StringBuilder(); 196 | for (int i = offset; i < data.length; i++) { 197 | if (data[i] == '\n') 198 | break; 199 | builder.append((char) data[i]); 200 | } 201 | return builder.toString(); 202 | } 203 | 204 | // For V2, challenge is always testChallenge. 205 | @Override 206 | public String getChallenge() { 207 | return testChallenge; 208 | } 209 | } 210 | 211 | @Test 212 | public void testV1Vote() throws Exception { 213 | // Construct a vote message for V1. 214 | String voteMsg = "VOTE\nvotifier.bencodez.com\ntestUser\n127.0.0.1\nTestTimestamp\n"; 215 | Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); 216 | cipher.init(Cipher.ENCRYPT_MODE, testKeyPair.getPublic()); 217 | byte[] encrypted = cipher.doFinal(voteMsg.getBytes(StandardCharsets.UTF_8)); 218 | assertEquals(256, encrypted.length); 219 | 220 | Vote vote = receiver.processV1Vote(encrypted); 221 | assertNotNull(vote); 222 | assertEquals("votifier.bencodez.com", vote.getServiceName()); 223 | assertEquals("testUser", vote.getUsername()); 224 | assertEquals("127.0.0.1", vote.getAddress()); 225 | assertEquals("TestTimestamp", vote.getTimeStamp()); 226 | vote.setSourceAddress("192.168.1.1"); // Add sourceAddress 227 | assertEquals("192.168.1.1", vote.getSourceAddress()); 228 | } 229 | 230 | @Test 231 | public void testV2Vote() throws Exception { 232 | // Construct a JSON payload for V2. 233 | String challenge = "testChallenge"; 234 | JsonObject inner = new JsonObject(); 235 | inner.addProperty("serviceName", "votifier.bencodez.com"); 236 | inner.addProperty("username", "testUserV2"); 237 | inner.addProperty("address", "127.0.0.1"); 238 | inner.addProperty("timestamp", "TestTimestampV2"); 239 | inner.addProperty("challenge", challenge); 240 | String payload = inner.toString(); 241 | 242 | // Compute HMAC signature using dummyTokenKey. 243 | Mac mac = Mac.getInstance("HmacSHA256"); // Declare and initialize mac 244 | mac.init(dummyTokenKey); 245 | byte[] signatureBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); 246 | String signature = Base64.getEncoder().encodeToString(signatureBytes); 247 | 248 | JsonObject outer = new JsonObject(); 249 | outer.addProperty("payload", payload); 250 | outer.addProperty("signature", signature); 251 | String jsonPayload = outer.toString(); 252 | 253 | // Create a new TestVoteReceiver in token mode. 254 | TestVoteReceiver tokenReceiver = new TestVoteReceiver("127.0.0.1", 0, testKeyPair) { 255 | @Override 256 | public boolean isUseTokens() { 257 | return true; 258 | } 259 | }; 260 | Vote vote = tokenReceiver.processV2Vote(jsonPayload); 261 | assertNotNull(vote); 262 | assertEquals("votifier.bencodez.com", vote.getServiceName()); 263 | assertEquals("testUserV2", vote.getUsername()); 264 | assertEquals("127.0.0.1", vote.getAddress()); 265 | assertEquals("TestTimestampV2", vote.getTimeStamp()); 266 | vote.setSourceAddress("192.168.1.2"); // Add sourceAddress 267 | assertEquals("192.168.1.2", vote.getSourceAddress()); 268 | tokenReceiver.shutdown(); 269 | } 270 | 271 | @Test 272 | public void testProxyV1Header() throws Exception { 273 | // Test processing of a PROXY protocol v1 header. 274 | String proxyHeader = "PROXY TCP4 192.168.1.1 192.168.1.2 1234 80\r\n"; 275 | String remainingData = "VOTE\nvotifier.bencodez.com\ntestUser\n127.0.0.1\nTestTimestamp\n"; 276 | String input = proxyHeader + remainingData; 277 | PushbackInputStream pis = new PushbackInputStream( 278 | new ByteArrayInputStream(input.getBytes(StandardCharsets.US_ASCII)), 512); 279 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 280 | BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(baos, StandardCharsets.US_ASCII)); 281 | 282 | // Use reflection to call the private processProxyHeaders method. 283 | Method method = VoteReceiver.class.getDeclaredMethod("processProxyHeaders", PushbackInputStream.class, 284 | BufferedWriter.class); 285 | method.setAccessible(true); 286 | method.invoke(receiver, pis, writer); 287 | 288 | // After processing, the remaining data should be the vote payload. 289 | byte[] remaining = new byte[remainingData.length()]; 290 | int read = pis.read(remaining); 291 | String output = new String(remaining, 0, read, StandardCharsets.US_ASCII); 292 | assertEquals(remainingData, output); 293 | } 294 | 295 | @Test 296 | public void testConnectHeader() throws Exception { 297 | // Test processing of an HTTP CONNECT header. 298 | String connectHeader = "CONNECT some.host:443 HTTP/1.1\r\nHost: some.host:443\r\n\r\n"; 299 | String remainingData = "VOTE\nvotifier.bencodez.com\ntestUser\n127.0.0.1\nTestTimestamp\n"; 300 | String input = connectHeader + remainingData; 301 | PushbackInputStream pis = new PushbackInputStream( 302 | new ByteArrayInputStream(input.getBytes(StandardCharsets.US_ASCII)), 512); 303 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 304 | BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(baos, StandardCharsets.US_ASCII)); 305 | 306 | Method method = VoteReceiver.class.getDeclaredMethod("processProxyHeaders", PushbackInputStream.class, 307 | BufferedWriter.class); 308 | method.setAccessible(true); 309 | method.invoke(receiver, pis, writer); 310 | writer.flush(); 311 | 312 | // The writer should contain the HTTP CONNECT response. 313 | String response = baos.toString("ASCII"); 314 | assertTrue(response.contains("200 Connection Established")); 315 | 316 | // The remaining data in the stream should be the vote payload. 317 | byte[] remaining = new byte[remainingData.length()]; 318 | int read = pis.read(remaining); 319 | String output = new String(remaining, 0, read, StandardCharsets.US_ASCII); 320 | assertEquals(remainingData, output); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /VotifierPlus/src/test/java/com/bencodez/votifierplus/tests/VoteTest.java: -------------------------------------------------------------------------------- 1 | package com.bencodez.votifierplus.tests; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.vexsoftware.votifier.model.Vote; 8 | 9 | public class VoteTest { 10 | @Test 11 | public void serviceNameIsSetCorrectly() { 12 | Vote vote = new Vote(); 13 | vote.setServiceName("TestService"); 14 | assertEquals("TestService", vote.getServiceName()); 15 | } 16 | 17 | @Test 18 | public void usernameIsSetCorrectly() { 19 | Vote vote = new Vote(); 20 | vote.setUsername("TestUser"); 21 | assertEquals("TestUser", vote.getUsername()); 22 | } 23 | 24 | @Test 25 | public void usernameIsTruncatedIfTooLong() { 26 | Vote vote = new Vote(); 27 | vote.setUsername("ThisUsernameIsWayTooLong"); 28 | assertEquals("ThisUsernameIsWa", vote.getUsername()); 29 | } 30 | 31 | @Test 32 | public void addressIsSetCorrectly() { 33 | Vote vote = new Vote(); 34 | vote.setAddress("127.0.0.1"); 35 | assertEquals("127.0.0.1", vote.getAddress()); 36 | } 37 | 38 | @Test 39 | public void timeStampIsSetCorrectly() { 40 | Vote vote = new Vote(); 41 | vote.setTimeStamp("2023-10-10 10:10:10"); 42 | assertEquals("2023-10-10 10:10:10", vote.getTimeStamp()); 43 | } 44 | 45 | @Test 46 | public void toStringReturnsCorrectFormat() { 47 | Vote vote = new Vote("TestService", "TestUser", "127.0.0.1", "2023-10-10 10:10:10"); 48 | vote.setSourceAddress("192.168.1.1"); // Set sourceAddress 49 | assertEquals("Vote (from:TestService username:TestUser address:127.0.0.1 timeStamp:2023-10-10 10:10:10, sourceAddress:192.168.1.1)", 50 | vote.toString()); 51 | } 52 | } 53 | --------------------------------------------------------------------------------