package net.limework.rediskript.managers; import ch.njol.skript.registrations.Classes; import ch.njol.skript.variables.Variables; import net.limework.rediskript.RediSkript; import net.limework.rediskript.data.Encryption; import net.limework.rediskript.events.RedisMessageEvent; import org.bukkit.Bukkit; import org.bukkit.configuration.Configuration; import org.bukkit.scheduler.BukkitTask; import org.cryptomator.siv.UnauthenticCiphertextException; import org.json.JSONArray; import org.json.JSONObject; import redis.clients.jedis.*; import redis.clients.jedis.exceptions.JedisConnectionException; import javax.crypto.IllegalBlockSizeException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; public class RedisController extends BinaryJedisPubSub implements Runnable { //Jedis Pool to be used by every another class. private final JedisPool jedisPool; //this seems useless unless tls is OFF! // class author is govindas :/ private final Encryption encryption; private byte[][] channelsInByte; private final AtomicBoolean isConnectionBroken; private final AtomicBoolean isConnecting; private final RediSkript plugin; private final BukkitTask ConnectionTask; public RedisController(RediSkript plugin) { this.plugin = plugin; Configuration config = plugin.getConfig(); JedisPoolConfig JConfig = new JedisPoolConfig(); int maxConnections = config.getInt("Redis.MaxConnections"); //do not allow less than 2 max connections as that causes issues if (maxConnections < 2) { maxConnections = 2; } JConfig.setMaxTotal(maxConnections); JConfig.setMaxIdle(maxConnections); JConfig.setMinIdle(1); JConfig.setBlockWhenExhausted(true); final String password = config.getString("Redis.Password", ""); if (password.isEmpty()) { this.jedisPool = new JedisPool(JConfig, config.getString("Redis.Host", "127.0.0.1"), config.getInt("Redis.Port", 6379), config.getInt("Redis.TimeOut", 9000), config.getBoolean("Redis.useTLS", false)); } else { this.jedisPool = new JedisPool(JConfig, config.getString("Redis.Host", "127.0.0.1"), config.getInt("Redis.Port", 6379), config.getInt("Redis.TimeOut", 9000), password, config.getBoolean("Redis.useTLS", false)); } encryption = new Encryption(config.getBoolean("Redis.EncryptMessages"), config.getString("Redis.EncryptionKey"), config.getString("Redis.MacKey")); setupChannels(config); isConnectionBroken = new AtomicBoolean(true); isConnecting = new AtomicBoolean(false); //Start the main task on async thread ConnectionTask = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, this, 0, 20 * 5); } @Override public void run() { if (!isConnectionBroken.get() || isConnecting.get()) { return; } plugin.sendLogs("Connecting to Redis server..."); isConnecting.set(true); try (Jedis jedis = jedisPool.getResource()) { isConnectionBroken.set(false); plugin.sendLogs("&aConnection to Redis server has established! Success!"); jedis.subscribe(this, channelsInByte); } catch (Exception e) { isConnecting.set(false); isConnectionBroken.set(true); plugin.sendErrorLogs("Connection to Redis server has failed! Please check your details in the configuration."); e.printStackTrace(); } } public void shutdown() { ConnectionTask.cancel(); if (this.isSubscribed()) { try { this.unsubscribe(); } catch (Exception e) { plugin.sendErrorLogs("Something went wrong during unsubscribing..."); e.printStackTrace(); } } jedisPool.close(); } @Override public void onMessage(byte[] channel, byte[] message) { String channelString = new String(channel, StandardCharsets.UTF_8); String receivedMessage = null; try { //if encryption is enabled, decrypt the message, else just convert binary to string if (this.encryption.isEncryptionEnabled()) { try { receivedMessage = encryption.decrypt(message); } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { e.printStackTrace(); } } else { //encryption is disabled, so let's just get the string receivedMessage = new String(message, StandardCharsets.UTF_8); } if (receivedMessage != null) { JSONObject j = new JSONObject(receivedMessage); if (j.get("Type").equals("Skript")) { JSONArray messages = j.getJSONArray("Messages"); RedisMessageEvent event; for (int i = 0; i < messages.length(); i++) { event = new RedisMessageEvent(channelString, messages.get(i).toString(), j.getLong("Date")); //if plugin is disabling, don't call events anymore if (plugin.isEnabled()) { RedisMessageEvent finalEvent = event; Bukkit.getScheduler().runTask(plugin, () -> plugin.getServer().getPluginManager().callEvent(finalEvent)); } } } else if (j.get("Type").equals("SkriptVariables")) { //Transfer variables between servers JSONArray varNames = j.getJSONArray("Names"); Object inputValue; String changeValue = null; JSONArray varValues = null; if (!j.isNull("Values")) { varValues = j.getJSONArray("Values"); } for (int i = 0; i < varNames.length(); i++) { String varName = varNames.get(i).toString(); if (j.isNull("Values")) { // only check for SET here, because null has to be ignored in all other cases if (j.getString("Operation").equals("SET")) { Variables.setVariable(varName, null, null, false); } } else { if (!varValues.isNull(i)) { changeValue = varValues.get(i).toString(); } String[] inputs = changeValue.split("\\^", 2); inputValue = Classes.deserialize(inputs[0], Base64.getDecoder().decode(inputs[1])); switch (j.getString("Operation")) { case "ADD": if (varName.charAt(varName.length() - 1) == '*') { plugin.getLogger().log(Level.WARNING, "Adding to {::*} variables in RediSkript is not supported. Variable name: " + varName); continue; } Object variable = Variables.getVariable(varName, null, false); if (variable == null) { Variables.setVariable(varName, inputValue, null, false); } else if (variable instanceof Long) { if (inputValue instanceof Long) { Variables.setVariable(varName, (Long) variable + (Long) inputValue, null, false); } else if (inputValue instanceof Double) { // convert Long variable to Double variable = Double.valueOf((Long) variable); Variables.setVariable(varName, (Double) variable + (Double) inputValue, null, false); } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported add action of data type (" + inputValue.getClass().getName() + ") on variable: " + varName); continue; } } else if (variable instanceof Double) { if (inputValue instanceof Double) { Variables.setVariable(varName, (Double) variable + (Double) inputValue, null, false); } else if (inputValue instanceof Long) { Variables.setVariable(varName, (Double) variable + ((Long) inputValue).doubleValue(), null, false); } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported add action of data type (" + inputValue.getClass().getName() + ") on variable: " + varName); continue; } } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported variable type in add action (" + variable.getClass().getName() + ") on variable: " + varName); continue; } break; case "REMOVE": if (varName.charAt(varName.length() - 1) == '*') { plugin.getLogger().log(Level.WARNING, "Removing from {::*} variables in RediSkript is not supported. Variable name: " + varName); continue; } variable = Variables.getVariable(varName, null, false); if (variable == null) { if (inputValue instanceof Long) { Variables.setVariable(varName, -(Long) inputValue, null, false); } else if (inputValue instanceof Double) { Variables.setVariable(varName, -(Double) inputValue, null, false); } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported remove action of data type (" + inputValue.getClass().getName() + ") on variable: " + varName); continue; } } else if (variable instanceof Long) { if (inputValue instanceof Long) { Variables.setVariable(varName, (Long) variable - (Long) inputValue, null, false); } else if (inputValue instanceof Double) { // convert Long variable to Double variable = Double.valueOf((Long) variable); Variables.setVariable(varName, (Double) variable - (Double) inputValue, null, false); } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported remove action of data type (" + inputValue.getClass().getName() + ") on variable: " + varName); continue; } } else if (variable instanceof Double) { if (inputValue instanceof Double) { Variables.setVariable(varName, (Double) variable - (Double) inputValue, null, false); } else if (inputValue instanceof Long) { Variables.setVariable(varName, (Double) variable - ((Long) inputValue).doubleValue(), null, false); } } else { // Not supported input type plugin.getLogger().log(Level.WARNING, "Unsupported variable type in remove action (" + variable.getClass().getName() + ") on variable: " + varName); continue; } break; case "SET": //this is needed, because setting a {variable::*} causes weird behavior, like //1st set operation is no data, 2nd has data, etc. //if you set it to null before action, it works correctly if (varName.charAt(varName.length() - 1) == '*') { Variables.setVariable(varName, null, null, false); } Variables.setVariable(varNames.get(i).toString(), inputValue, null, false); break; } } } } } } catch (Exception e) { plugin.sendErrorLogs("&cI got a message that was empty from channel " + channelString + " please check your code that you used to send the message. Message content:"); plugin.sendErrorLogs(receivedMessage); e.printStackTrace(); } } public void sendMessage(String[] message, String channel) { JSONObject json = new JSONObject(); json.put("Messages", new JSONArray(message)); json.put("Type", "Skript"); json.put("Date", System.currentTimeMillis()); //for unique string every time & PING calculations finishSendMessage(json, channel); } public void sendVariables(String[] variableNames, String[] variableValues, String channel, String operation) { JSONObject json = new JSONObject(); json.put("Names", new JSONArray(variableNames)); if (variableValues != null) { json.put("Values", new JSONArray(variableValues)); } json.put("Type", "SkriptVariables"); json.put("Date", System.currentTimeMillis()); //for unique string every time & PING calculations json.put("Operation", operation); finishSendMessage(json, channel); } public void finishSendMessage(JSONObject json, String channel) { try { byte[] message; if (encryption.isEncryptionEnabled()) { message = encryption.encrypt(json.toString()); } else { message = json.toString().getBytes(StandardCharsets.UTF_8); } //sending a redis message blocks main thread if there's no more connections available //so to avoid issues, it's best to do it always on separate thread if (plugin.isEnabled()) { plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Jedis j = jedisPool.getResource()) { j.publish(channel.getBytes(StandardCharsets.UTF_8), message); } catch (Exception e) { plugin.sendErrorLogs("Error sending redis message!"); e.printStackTrace(); } }); } else { //execute sending of redis message on the main thread if plugin is disabling //so it can still process the sending try (Jedis j = jedisPool.getResource()) { j.publish(channel.getBytes(StandardCharsets.UTF_8), message); } catch (Exception e) { e.printStackTrace(); } } } catch (JedisConnectionException exception) { exception.printStackTrace(); } } private void setupChannels(Configuration config) { List channels = config.getStringList("Channels"); channelsInByte = new byte[channels.size()][1]; for (int x = 0; x < channels.size(); x++) { channelsInByte[x] = channels.get(x).getBytes(StandardCharsets.UTF_8); } } public Boolean isRedisConnectionOffline() { return isConnectionBroken.get(); } // for skript reflect :) public JedisPool getJedisPool() { return jedisPool; } // }