() {
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 extends ConfigurationNode> 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 extends ConfigurationNode> 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 |
--------------------------------------------------------------------------------