2
0
mirror of https://github.com/proxiodev/RedisBungee.git synced 2026-04-09 00:20:26 +00:00

seperate the internals from bungeecord api for easily supporting platform | progress 60%

This commit is contained in:
2022-04-13 17:14:08 +04:00
parent 165ba84791
commit 61dec7f03c
36 changed files with 945 additions and 1937 deletions

View File

@@ -0,0 +1,325 @@
package com.imaginarycode.minecraft.redisbungee;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent;
import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.net.InetAddress;
import java.util.*;
/**
* This class exposes some internal RedisBungee functions. You obtain an instance of this object by invoking {@link RedisBungeePlugin#getApi()}.
*
* @author tuxed
* @since 0.2.3
*
*/
@SuppressWarnings("unused")
public class RedisBungeeAPI {
private final RedisBungeePlugin<?> plugin;
private final List<String> reservedChannels;
private static RedisBungeeAPI redisBungeeApi;
RedisBungeeAPI(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
redisBungeeApi = this;
this.reservedChannels = ImmutableList.of(
"redisbungee-allservers",
"redisbungee-" + plugin.getConfiguration().getServerId(),
"redisbungee-data"
);
}
/**
* Get a combined count of all players on this network.
*
* @return a count of all players found
*/
public final int getPlayerCount() {
return plugin.getCount();
}
/**
* Get the last time a player was on. If the player is currently online, this will return 0. If the player has not been recorded,
* this will return -1. Otherwise it will return a value in milliseconds.
*
* @param player a player name
* @return the last time a player was on, if online returns a 0
*/
public final long getLastOnline(@NonNull UUID player) {
return plugin.getDataManager().getLastOnline(player);
}
/**
* Get the server where the specified player is playing. This function also deals with the case of local players
* as well, and will return local information on them.
*
* @param player a player name
* @return a String name for the server the player is on.
*/
public final String getServerFor(@NonNull UUID player) {
return plugin.getDataManager().getServer(player);
}
/**
* Get a combined list of players on this network.
* <p>
* <strong>Note that this function returns an instance of {@link com.google.common.collect.ImmutableSet}.</strong>
*
* @return a Set with all players found
*/
public final Set<UUID> getPlayersOnline() {
return plugin.getPlayers();
}
/**
* Get a combined list of players on this network, as a collection of usernames.
*
* @return a Set with all players found
* @see #getNameFromUuid(java.util.UUID)
* @since 0.3
*/
public final Collection<String> getHumanPlayersOnline() {
Set<String> names = new HashSet<>();
for (UUID uuid : getPlayersOnline()) {
names.add(getNameFromUuid(uuid, false));
}
return names;
}
/**
* Get a full list of players on all servers.
*
* @return a immutable Multimap with all players found on this server
* @since 0.2.5
*/
public final Multimap<String, UUID> getServerToPlayers() {
return plugin.serversToPlayers();
}
/**
* Get a list of players on the server with the given name.
*
* @param server a server name
* @return a Set with all players found on this server
*/
public final Set<UUID> getPlayersOnServer(@NonNull String server) {
return ImmutableSet.copyOf(getServerToPlayers().get(server));
}
/**
* Get a list of players on the specified proxy.
*
* @param server a server name
* @return a Set with all UUIDs found on this proxy
*/
public final Set<UUID> getPlayersOnProxy(@NonNull String server) {
return plugin.getPlayersOnProxy(server);
}
/**
* Convenience method: Checks if the specified player is online.
*
* @param player a player name
* @return if the player is online
*/
public final boolean isPlayerOnline(@NonNull UUID player) {
return getLastOnline(player) == 0;
}
/**
* Get the {@link java.net.InetAddress} associated with this player.
*
* @param player the player to fetch the IP for
* @return an {@link java.net.InetAddress} if the player is online, null otherwise
* @since 0.2.4
*/
public final InetAddress getPlayerIp(@NonNull UUID player) {
return plugin.getDataManager().getIp(player);
}
/**
* Get the RedisBungee proxy ID this player is connected to.
*
* @param player the player to fetch the IP for
* @return the proxy the player is connected to, or null if they are offline
* @since 0.3.3
*/
public final String getProxy(@NonNull UUID player) {
return plugin.getDataManager().getProxy(player);
}
/**
* Sends a proxy command to all proxies.
*
* @param command the command to send and execute
* @see #sendProxyCommand(String, String)
* @since 0.2.5
*/
public final void sendProxyCommand(@NonNull String command) {
plugin.sendProxyCommand("allservers", command);
}
/**
* Sends a proxy command to the proxy with the given ID. "allservers" means all proxies.
*
* @param proxyId a proxy ID
* @param command the command to send and execute
* @see #getServerId()
* @see #getAllServers()
* @since 0.2.5
*/
public final void sendProxyCommand(@NonNull String proxyId, @NonNull String command) {
plugin.sendProxyCommand(proxyId, command);
}
/**
* Sends a message to a PubSub channel. The channel has to be subscribed to on this, or another redisbungee instance for {@link PubSubMessageEvent} to fire.
*
* @param channel The PubSub channel
* @param message the message body to send
* @since 0.3.3
*/
public final void sendChannelMessage(@NonNull String channel, @NonNull String message) {
plugin.sendChannelMessage(channel, message);
}
/**
* Get the current BungeeCord server ID for this server.
*
* @return the current server ID
* @see #getAllServers()
* @since 0.2.5
*/
public final String getServerId() {
return plugin.getConfiguration().getServerId();
}
/**
* Get all the linked proxies in this network.
*
* @return the list of all proxies
* @see #getServerId()
* @since 0.2.5
*/
public final List<String> getAllServers() {
return plugin.getServerIds();
}
/**
* Register (a) PubSub channel(s), so that you may handle {@link PubSubMessageEvent} for it.
*
* @param channels the channels to register
* @since 0.3
*/
public final void registerPubSubChannels(String... channels) {
plugin.getPubSubListener().addChannel(channels);
}
/**
* Unregister (a) PubSub channel(s).
*
* @param channels the channels to unregister
* @since 0.3
*/
public final void unregisterPubSubChannels(String... channels) {
for (String channel : channels) {
Preconditions.checkArgument(!reservedChannels.contains(channel), "attempting to unregister internal channel");
}
plugin.getPubSubListener().removeChannel(channels);
}
/**
* Fetch a name from the specified UUID. UUIDs are cached locally and in Redis. This function falls back to Mojang
* as a last resort, so calls <strong>may</strong> be blocking.
* <p>
* For the common use case of translating a list of UUIDs into names, use {@link #getHumanPlayersOnline()} instead.
* <p>
* If performance is a concern, use {@link #getNameFromUuid(java.util.UUID, boolean)} as this allows you to disable Mojang lookups.
*
* @param uuid the UUID to fetch the name for
* @return the name for the UUID
* @since 0.3
*/
public final String getNameFromUuid(@NonNull UUID uuid) {
return getNameFromUuid(uuid, true);
}
/**
* Fetch a name from the specified UUID. UUIDs are cached locally and in Redis. This function can fall back to Mojang
* as a last resort if {@code expensiveLookups} is true, so calls <strong>may</strong> be blocking.
* <p>
* For the common use case of translating the list of online players into names, use {@link #getHumanPlayersOnline()}.
* <p>
* If performance is a concern, set {@code expensiveLookups} to false as this will disable lookups via Mojang.
*
* @param uuid the UUID to fetch the name for
* @param expensiveLookups whether or not to perform potentially expensive lookups
* @return the name for the UUID
* @since 0.3.2
*/
public final String getNameFromUuid(@NonNull UUID uuid, boolean expensiveLookups) {
return plugin.getUuidTranslator().getNameFromUuid(uuid, expensiveLookups);
}
/**
* Fetch a UUID from the specified name. Names are cached locally and in Redis. This function falls back to Mojang
* as a last resort, so calls <strong>may</strong> be blocking.
* <p>
* If performance is a concern, see {@link #getUuidFromName(String, boolean)}, which disables the following functions:
* <ul>
* <li>Searching local entries case-insensitively</li>
* <li>Searching Mojang</li>
* </ul>
*
* @param name the UUID to fetch the name for
* @return the UUID for the name
* @since 0.3
*/
public final UUID getUuidFromName(@NonNull String name) {
return getUuidFromName(name, true);
}
/**
* Fetch a UUID from the specified name. Names are cached locally and in Redis. This function falls back to Mojang
* as a last resort if {@code expensiveLookups} is true, so calls <strong>may</strong> be blocking.
* <p>
* If performance is a concern, set {@code expensiveLookups} to false to disable searching Mojang and searching for usernames
* case-insensitively.
*
* @param name the UUID to fetch the name for
* @param expensiveLookups whether or not to perform potentially expensive lookups
* @return the UUID for the name
* @since 0.3.2
*/
public final UUID getUuidFromName(@NonNull String name, boolean expensiveLookups) {
return plugin.getUuidTranslator().getTranslatedUuid(name, expensiveLookups);
}
/**
* This gives you instance of Jedis!
*
* @return {@link JedisPool}
* @since 0.7.0
*/
public Jedis getJedisPool() {
return this.plugin.requestJedis();
}
/**
*
* @return the API instance.
* @since 0.6.5
*/
public static RedisBungeeAPI getRedisBungeeApi() {
return redisBungeeApi;
}
}

View File

@@ -0,0 +1,27 @@
package com.imaginarycode.minecraft.redisbungee.events;
/**
* This event is posted when a PubSub message is received.
* <p>
* <strong>Warning</strong>: This event is fired in a separate thread!
*
* @since 0.2.6
*/
public class PubSubMessageEvent {
private final String channel;
private final String message;
public PubSubMessageEvent(String channel, String message) {
this.channel = channel;
this.message = message;
}
public String getChannel() {
return channel;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,70 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.io.ByteArrayDataOutput;
import com.google.gson.Gson;
import java.net.InetAddress;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public abstract class AbstractRedisBungeeListener<LE, PLE, PD, SC, PP, PM, PS> {
protected static final String ALREADY_LOGGED_IN = "§cYou are already logged on to this server. \n\nIt may help to try logging in again in a few minutes.\nIf this does not resolve your issue, please contact staff.";
protected static final String ONLINE_MODE_RECONNECT = "§cWhoops! You need to reconnect\n\nWe found someone online using your username. They were kicked and you may reconnect.\nIf this does not work, please contact staff.";
protected final RedisBungeePlugin<?> plugin;
protected final List<InetAddress> exemptAddresses;
protected final Gson gson = new Gson();
public AbstractRedisBungeeListener(RedisBungeePlugin<?> plugin, List<InetAddress> exemptAddresses) {
this.plugin = plugin;
this.exemptAddresses = exemptAddresses;
}
public abstract void onLogin(LE event);
public abstract void onPostLogin(PLE event);
public abstract void onPlayerDisconnect(PD event);
public abstract void onServerChange(SC event);
public abstract void onPing(PP event);
public abstract void onPluginMessage(PM event);
private void serializeMultiset(Multiset<String> collection, ByteArrayDataOutput output) {
output.writeInt(collection.elementSet().size());
for (Multiset.Entry<String> entry : collection.entrySet()) {
output.writeUTF(entry.getElement());
output.writeInt(entry.getCount());
}
}
@SuppressWarnings("SameParameterValue")
private void serializeMultimap(Multimap<String, String> collection, boolean includeNames, ByteArrayDataOutput output) {
output.writeInt(collection.keySet().size());
for (Map.Entry<String, Collection<String>> entry : collection.asMap().entrySet()) {
output.writeUTF(entry.getKey());
if (includeNames) {
serializeCollection(entry.getValue(), output);
} else {
output.writeInt(entry.getValue().size());
}
}
}
private void serializeCollection(Collection<?> collection, ByteArrayDataOutput output) {
output.writeInt(collection.size());
for (Object o : collection) {
output.writeUTF(o.toString());
}
}
public abstract void onPubSubMessage(PS event);
}

View File

@@ -0,0 +1,299 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.net.InetAddresses;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent;
import redis.clients.jedis.Jedis;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* This class manages all the data that RedisBungee fetches from Redis, along with updates to that data.
*
* @since 0.3.3
*/
public abstract class DataManager<P, PS, PL, PD> {
private final RedisBungeePlugin<P> plugin;
private final Cache<UUID, String> serverCache = createCache();
private final Cache<UUID, String> proxyCache = createCache();
private final Cache<UUID, InetAddress> ipCache = createCache();
private final Cache<UUID, Long> lastOnlineCache = createCache();
private final Gson gson = new Gson();
public DataManager(RedisBungeePlugin<P> plugin) {
this.plugin = plugin;
}
private static <K, V> Cache<K, V> createCache() {
// TODO: Allow customization via cache specification, ala ServerListPlus
return CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
private final JsonParser parser = new JsonParser();
public String getServer(final UUID uuid) {
P player = plugin.getPlayer(uuid);
if (player != null)
return plugin.isPlayerOnAServer(player) ? plugin.getPlayerServerName(player) : null;
try {
return serverCache.get(uuid, new Callable<String>() {
@Override
public String call() throws Exception {
try (Jedis tmpRsc = plugin.requestJedis()) {
return Objects.requireNonNull(tmpRsc.hget("player:" + uuid, "server"), "user not found");
}
}
});
} catch (ExecutionException | UncheckedExecutionException e) {
if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found"))
return null; // HACK
plugin.logFatal("Unable to get server");
throw new RuntimeException("Unable to get server for " + uuid, e);
}
}
public String getProxy(final UUID uuid) {
P player = plugin.getPlayer(uuid);
if (player != null)
return plugin.getConfiguration().getServerId();
try {
return proxyCache.get(uuid, new Callable<String>() {
@Override
public String call() throws Exception {
try (Jedis tmpRsc = plugin.requestJedis()) {
return Objects.requireNonNull(tmpRsc.hget("player:" + uuid, "proxy"), "user not found");
}
}
});
} catch (ExecutionException | UncheckedExecutionException e) {
if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found"))
return null; // HACK
plugin.logFatal("Unable to get proxy");
throw new RuntimeException("Unable to get proxy for " + uuid, e);
}
}
public InetAddress getIp(final UUID uuid) {
P player = plugin.getPlayer(uuid);
if (player != null)
return plugin.getPlayerIp(player);
try {
return ipCache.get(uuid, new Callable<InetAddress>() {
@Override
public InetAddress call() throws Exception {
try (Jedis tmpRsc = plugin.requestJedis()) {
String result = tmpRsc.hget("player:" + uuid, "ip");
if (result == null)
throw new NullPointerException("user not found");
return InetAddresses.forString(result);
}
}
});
} catch (ExecutionException | UncheckedExecutionException e) {
if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found"))
return null; // HACK
plugin.logFatal( "Unable to get IP");
throw new RuntimeException("Unable to get IP for " + uuid, e);
}
}
public long getLastOnline(final UUID uuid) {
P player = plugin.getPlayer(uuid);
if (player != null)
return 0;
try {
return lastOnlineCache.get(uuid, new Callable<Long>() {
@Override
public Long call() throws Exception {
try (Jedis tmpRsc = plugin.requestJedis()) {
String result = tmpRsc.hget("player:" + uuid, "online");
return result == null ? -1 : Long.valueOf(result);
}
}
});
} catch (ExecutionException e) {
plugin.logFatal("Unable to get last time online");
throw new RuntimeException("Unable to get last time online for " + uuid, e);
}
}
private void invalidate(UUID uuid) {
ipCache.invalidate(uuid);
lastOnlineCache.invalidate(uuid);
serverCache.invalidate(uuid);
proxyCache.invalidate(uuid);
}
public void onPostLogin(PL event) {
}
public void onPlayerDisconnect(PD event) {
}
public abstract void onPubSubMessage(PS event);
protected void handlePubSubMessage(String channel, String message) {
if (!channel.equals("redisbungee-data"))
return;
// Partially deserialize the message so we can look at the action
JsonObject jsonObject = parser.parse(message).getAsJsonObject();
String source = jsonObject.get("source").getAsString();
if (source.equals(plugin.getConfiguration().getServerId()))
return;
DataManagerMessage.Action action = DataManagerMessage.Action.valueOf(jsonObject.get("action").getAsString());
switch (action) {
case JOIN:
final DataManagerMessage message1 = gson.fromJson(jsonObject, new TypeToken<DataManagerMessage>() {
}.getType());
proxyCache.put(message1.getTarget(), message1.getSource());
lastOnlineCache.put(message1.getTarget(), (long) 0);
ipCache.put(message1.getTarget(), ((LoginPayload)message1.getPayload()).getAddress());
plugin.executeAsync(new Runnable() {
@Override
public void run() {
//plugin.getProxy().getPluginManager().callEvent(new PlayerJoinedNetworkEvent(message1.getTarget()));
}
});
break;
case LEAVE:
final DataManagerMessage message2 = gson.fromJson(jsonObject, new TypeToken<DataManagerMessage>() {
}.getType());
invalidate(message2.getTarget());
lastOnlineCache.put(message2.getTarget(), ((LogoutPayload)message2.getPayload()).getTimestamp());
plugin.executeAsync(new Runnable() {
@Override
public void run() {
// plugin.getProxy().getPluginManager().callEvent(new PlayerLeftNetworkEvent(message2.getTarget()));
}
});
break;
case SERVER_CHANGE:
final DataManagerMessage message3 = gson.fromJson(jsonObject, new TypeToken<DataManagerMessage>() {
}.getType());
serverCache.put(message3.getTarget(), ((ServerChangePayload)message3.getPayload()).getServer());
plugin.executeAsync(new Runnable() {
@Override
public void run() {
//plugin.getProxy().getPluginManager().callEvent(new PlayerChangedServerNetworkEvent(message3.getTarget(), message3.getPayload().getOldServer(), message3.getPayload().getServer()));
}
});
break;
}
}
public static class DataManagerMessage {
private final UUID target;
private final String source;
private final Action action; // for future use!
private final Payload payload;
public DataManagerMessage(UUID target, String source, Action action, Payload payload) {
this.target = target;
this.source = source;
this.action = action;
this.payload = payload;
}
public UUID getTarget() {
return target;
}
public String getSource() {
return source;
}
public Action getAction() {
return action;
}
public Payload getPayload() {
return payload;
}
public enum Action {
JOIN,
LEAVE,
SERVER_CHANGE
}
}
public static abstract class Payload {
}
public static class LoginPayload extends Payload{
private final InetAddress address;
public LoginPayload(InetAddress address) {
this.address = address;
}
public InetAddress getAddress() {
return address;
}
}
public static class ServerChangePayload extends Payload{
private final String server;
private final String oldServer;
ServerChangePayload(String server, String oldServer) {
this.server = server;
this.oldServer = oldServer;
}
public String getServer() {
return server;
}
public String getOldServer() {
return oldServer;
}
}
public static class LogoutPayload extends Payload {
private final long timestamp;
public LogoutPayload(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
}

View File

@@ -0,0 +1,47 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent;
import redis.clients.jedis.JedisPubSub;
import java.lang.reflect.InvocationTargetException;
public class JedisPubSubHandler extends JedisPubSub {
private final RedisBungeePlugin<?> plugin;
public JedisPubSubHandler(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
private Class<?> bungeeEvent;
@Override
public void onMessage(final String s, final String s2) {
if (s2.trim().length() == 0) return;
plugin.executeAsync(new Runnable() {
@Override
public void run() {
if (isBungeeEvent()) {
try {
Object object = bungeeEvent.getConstructor(String.class, String.class).newInstance(s, s2);
plugin.callEvent(object);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
throw new RuntimeException("unable to fire pubsub event.");
}
return;
}
PubSubMessageEvent event = new PubSubMessageEvent(s, s2);
plugin.callEvent(event);
}
});
}
public boolean isBungeeEvent() {
return bungeeEvent != null;
}
public void setBungeeEvent(Class<?> clazz) {
bungeeEvent = clazz;
}
}

View File

@@ -0,0 +1,71 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class PubSubListener implements Runnable {
private JedisPubSubHandler jpsh;
private Set<String> addedChannels = new HashSet<String>();
private final RedisBungeePlugin<?> plugin;
public PubSubListener(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
@Override
public void run() {
boolean broken = false;
try (Jedis rsc = plugin.requestJedis()) {
try {
jpsh = new JedisPubSubHandler(plugin);
addedChannels.add("redisbungee-" + plugin.getConfiguration().getServerId());
addedChannels.add("redisbungee-allservers");
addedChannels.add("redisbungee-data");
rsc.subscribe(jpsh, addedChannels.toArray(new String[0]));
} catch (Exception e) {
// FIXME: Extremely ugly hack
// Attempt to unsubscribe this instance and try again.
plugin.logWarn("PubSub error, attempting to recover.");
try {
jpsh.unsubscribe();
} catch (Exception e1) {
/* This may fail with
- java.net.SocketException: Broken pipe
- redis.clients.jedis.exceptions.JedisConnectionException: JedisPubSub was not subscribed to a Jedis instance
*/
}
broken = true;
}
} catch (JedisConnectionException e) {
plugin.logWarn("PubSub error, attempting to recover in 5 secs.");
plugin.executeAsyncAfter(this, TimeUnit.SECONDS, 5);
}
if (broken) {
run();
}
}
public void addChannel(String... channel) {
addedChannels.addAll(Arrays.asList(channel));
jpsh.subscribe(channel);
}
public void removeChannel(String... channel) {
Arrays.asList(channel).forEach(addedChannels::remove);
jpsh.unsubscribe(channel);
}
public void poison() {
addedChannels.clear();
jpsh.unsubscribe();
}
}

View File

@@ -0,0 +1,36 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InetAddresses;
import java.net.InetAddress;
import java.util.List;
public class RedisBungeeConfiguration {
private final String serverId;
private final List<InetAddress> exemptAddresses;
private static RedisBungeeConfiguration config;
public RedisBungeeConfiguration(String serverId, List<String> exemptAddresses) {
this.serverId = serverId;
ImmutableList.Builder<InetAddress> addressBuilder = ImmutableList.builder();
for (String s : exemptAddresses) {
addressBuilder.add(InetAddresses.forString(s));
}
this.exemptAddresses = addressBuilder.build();
config = this;
}
public String getServerId() {
return serverId;
}
public List<InetAddress> getExemptAddresses() {
return exemptAddresses;
}
public static RedisBungeeConfiguration getConfig() {
return config;
}
}

View File

@@ -0,0 +1,79 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.google.common.collect.Multimap;
import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI;
import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.UUIDTranslator;
import redis.clients.jedis.Jedis;
import java.net.InetAddress;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public interface RedisBungeePlugin<P> {
void enable();
void disable();
RedisBungeeConfiguration getConfiguration();
int getCount();
DataManager<P, ?, ?, ?> getDataManager();
Set<UUID> getPlayers();
Jedis requestJedis();
RedisBungeeAPI getApi();
UUIDTranslator getUuidTranslator();
Multimap<String, UUID> serversToPlayers();
Set<UUID> getPlayersOnProxy(String proxyId);
void sendProxyCommand(String serverId, String command);
List<String> getServerIds();
PubSubListener getPubSubListener();
void sendChannelMessage(String channel, String message);
void executeAsync(Runnable runnable);
void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int seconds);
void callEvent(Object object);
boolean isOnlineMode();
void logInfo(String msg);
void logWarn(String msg);
void logFatal(String msg);
boolean isPlayerServerNull(P player);
P getPlayer(UUID uuid);
P getPlayer(String name);
UUID getPlayerUUID(String player);
String getPlayerName(UUID player);
String getPlayerServerName(P player);
boolean isPlayerOnAServer(P player);
InetAddress getPlayerIp(P player);
void executeProxyCommand(String cmd);
}

View File

@@ -0,0 +1,51 @@
package com.imaginarycode.minecraft.redisbungee.internal;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.Gson;
import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import java.util.UUID;
@VisibleForTesting
public class RedisUtil {
private static final Gson gson = new Gson();
public static void cleanUpPlayer(String player, Jedis rsc) {
rsc.srem("proxy:" + RedisBungeeAPI.getRedisBungeeApi().getServerId() + ":usersOnline", player);
rsc.hdel("player:" + player, "server", "ip", "proxy");
long timestamp = System.currentTimeMillis();
rsc.hset("player:" + player, "online", String.valueOf(timestamp));
rsc.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage(
UUID.fromString(player), RedisBungeeAPI.getRedisBungeeApi().getServerId(), DataManager.DataManagerMessage.Action.LEAVE,
new DataManager.LogoutPayload(timestamp))));
}
public static void cleanUpPlayer(String player, Pipeline rsc) {
rsc.srem("proxy:" + RedisBungeeAPI.getRedisBungeeApi().getServerId() + ":usersOnline", player);
rsc.hdel("player:" + player, "server", "ip", "proxy");
long timestamp = System.currentTimeMillis();
rsc.hset("player:" + player, "online", String.valueOf(timestamp));
rsc.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage(
UUID.fromString(player), RedisBungeeAPI.getRedisBungeeApi().getServerId(), DataManager.DataManagerMessage.Action.LEAVE,
new DataManager.LogoutPayload(timestamp))));
}
public static boolean isRedisVersionRight(String redisVersion) {
// Need to use >=6.2 to use Lua optimizations.
String[] args = redisVersion.split("\\.");
if (args.length < 2) {
return false;
}
int major = Integer.parseInt(args[0]);
int minor = Integer.parseInt(args[1]);
return major >= 6 && minor >= 0;
}
// Ham1255: i am keeping this if some plugin uses this *IF*
@Deprecated
public static boolean canUseLua(String redisVersion) {
return isRedisVersionRight(redisVersion);
}
}

View File

@@ -0,0 +1,20 @@
package com.imaginarycode.minecraft.redisbungee.internal.util;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class IOUtil {
public static String readInputStreamAsString(InputStream is) {
String string;
try {
string = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new AssertionError(e);
}
return string;
}
}

View File

@@ -0,0 +1,58 @@
package com.imaginarycode.minecraft.redisbungee.internal.util;
import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisDataException;
import java.util.List;
public class LuaManager {
private final RedisBungeePlugin<?> plugin;
public LuaManager(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
public Script createScript(String script) {
try (Jedis jedis = plugin.requestJedis()) {
String hash = jedis.scriptLoad(script);
return new Script(script, hash);
}
}
public class Script {
private final String script;
private final String hashed;
public Script(String script, String hashed) {
this.script = script;
this.hashed = hashed;
}
public String getScript() {
return script;
}
public String getHashed() {
return hashed;
}
public Object eval(List<String> keys, List<String> args) {
Object data;
try (Jedis jedis = plugin.requestJedis()) {
try {
data = jedis.evalsha(hashed, keys, args);
} catch (JedisDataException e) {
if (e.getMessage().startsWith("NOSCRIPT")) {
data = jedis.eval(script, keys, args);
} else {
throw e;
}
}
}
return data;
}
}
}

View File

@@ -0,0 +1,48 @@
package com.imaginarycode.minecraft.redisbungee.internal.util;
import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.concurrent.Callable;
public abstract class RedisCallable<T> implements Callable<T>, Runnable {
private final RedisBungeePlugin<?> plugin;
public RedisCallable(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
@Override
public T call() {
return run(false);
}
public void run() {
call();
}
private T run(boolean retry) {
try (Jedis jedis = plugin.requestJedis()) {
return call(jedis);
} catch (JedisConnectionException e) {
plugin.logFatal("Unable to get connection");
if (!retry) {
// Wait one second before retrying the task
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
throw new RuntimeException("task failed to run", e1);
}
return run(true);
}
}
throw new RuntimeException("task failed to run");
}
protected abstract T call(Jedis jedis);
}

View File

@@ -0,0 +1,44 @@
package com.imaginarycode.minecraft.redisbungee.internal.util.uuid;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class NameFetcher {
private static OkHttpClient httpClient;
private static final Gson gson = new Gson();
public static void setHttpClient(OkHttpClient httpClient) {
NameFetcher.httpClient = httpClient;
}
public static List<String> nameHistoryFromUuid(UUID uuid) throws IOException {
String url = "https://api.mojang.com/user/profiles/" + uuid.toString().replace("-", "") + "/names";
Request request = new Request.Builder().url(url).get().build();
ResponseBody body = httpClient.newCall(request).execute().body();
String response = body.string();
body.close();
Type listType = new TypeToken<List<Name>>() {
}.getType();
List<Name> names = gson.fromJson(response, listType);
List<String> humanNames = new ArrayList<>();
for (Name name : names) {
humanNames.add(name.name);
}
return humanNames;
}
public static class Name {
private String name;
private long changedToAt;
}
}

View File

@@ -0,0 +1,67 @@
package com.imaginarycode.minecraft.redisbungee.internal.util.uuid;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.squareup.okhttp.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/* Credits to evilmidget38 for this class. I modified it to use Gson. */
public class UUIDFetcher implements Callable<Map<String, UUID>> {
private static final double PROFILES_PER_REQUEST = 100;
private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft";
private static final MediaType JSON = MediaType.parse("application/json");
private final List<String> names;
private final boolean rateLimiting;
private static final Gson gson = new Gson();
public static void setHttpClient(OkHttpClient httpClient) {
UUIDFetcher.httpClient = httpClient;
}
private static OkHttpClient httpClient;
private UUIDFetcher(List<String> names, boolean rateLimiting) {
this.names = ImmutableList.copyOf(names);
this.rateLimiting = rateLimiting;
}
public UUIDFetcher(List<String> names) {
this(names, true);
}
public static UUID getUUID(String id) {
return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32));
}
public Map<String, UUID> call() throws Exception {
Map<String, UUID> uuidMap = new HashMap<>();
int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST);
for (int i = 0; i < requests; i++) {
String body = gson.toJson(names.subList(i * 100, Math.min((i + 1) * 100, names.size())));
Request request = new Request.Builder().url(PROFILE_URL).post(RequestBody.create(JSON, body)).build();
ResponseBody responseBody = httpClient.newCall(request).execute().body();
String response = responseBody.string();
responseBody.close();
Profile[] array = gson.fromJson(response, Profile[].class);
for (Profile profile : array) {
UUID uuid = UUIDFetcher.getUUID(profile.id);
uuidMap.put(profile.name, uuid);
}
if (rateLimiting && i != requests - 1) {
Thread.sleep(100L);
}
}
return uuidMap;
}
private static class Profile {
String id;
String name;
}
}

View File

@@ -0,0 +1,217 @@
package com.imaginarycode.minecraft.redisbungee.internal.util.uuid;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gson.Gson;
import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.exceptions.JedisException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.regex.Pattern;
public final class UUIDTranslator {
private static final Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}");
private static final Pattern MOJANGIAN_UUID_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");
private final RedisBungeePlugin<?> plugin;
private final Map<String, CachedUUIDEntry> nameToUuidMap = new ConcurrentHashMap<>(128, 0.5f, 4);
private final Map<UUID, CachedUUIDEntry> uuidToNameMap = new ConcurrentHashMap<>(128, 0.5f, 4);
private static final Gson gson = new Gson();
public UUIDTranslator(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
private void addToMaps(String name, UUID uuid) {
// This is why I like LocalDate...
// Cache the entry for three days.
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 3);
// Create the entry and populate the local maps
CachedUUIDEntry entry = new CachedUUIDEntry(name, uuid, calendar);
nameToUuidMap.put(name.toLowerCase(), entry);
uuidToNameMap.put(uuid, entry);
}
public final UUID getTranslatedUuid(@NonNull String player, boolean expensiveLookups) {
// If the player is online, give them their UUID.
// Remember, local data > remote data.
if (plugin.getPlayer(player) != null)
return plugin.getPlayerUUID(player);
// Check if it exists in the map
CachedUUIDEntry cachedUUIDEntry = nameToUuidMap.get(player.toLowerCase());
if (cachedUUIDEntry != null) {
if (!cachedUUIDEntry.expired())
return cachedUUIDEntry.getUuid();
else
nameToUuidMap.remove(player);
}
// Check if we can exit early
if (UUID_PATTERN.matcher(player).find()) {
return UUID.fromString(player);
}
if (MOJANGIAN_UUID_PATTERN.matcher(player).find()) {
// Reconstruct the UUID
return UUIDFetcher.getUUID(player);
}
// If we are in offline mode, UUID generation is simple.
// We don't even have to cache the UUID, since this is easy to recalculate.
if (!plugin.isOnlineMode()) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + player).getBytes(Charsets.UTF_8));
}
// Let's try Redis.
try (Jedis jedis = plugin.requestJedis()) {
String stored = jedis.hget("uuid-cache", player.toLowerCase());
if (stored != null) {
// Found an entry value. Deserialize it.
CachedUUIDEntry entry = gson.fromJson(stored, CachedUUIDEntry.class);
// Check for expiry:
if (entry.expired()) {
jedis.hdel("uuid-cache", player.toLowerCase());
// Doesn't hurt to also remove the UUID entry as well.
jedis.hdel("uuid-cache", entry.getUuid().toString());
} else {
nameToUuidMap.put(player.toLowerCase(), entry);
uuidToNameMap.put(entry.getUuid(), entry);
return entry.getUuid();
}
}
// That didn't work. Let's ask Mojang.
if (!expensiveLookups || !plugin.isOnlineMode())
return null;
Map<String, UUID> uuidMap1;
try {
uuidMap1 = new UUIDFetcher(Collections.singletonList(player)).call();
} catch (Exception e) {
plugin.logFatal("Unable to fetch UUID from Mojang for " + player);
return null;
}
for (Map.Entry<String, UUID> entry : uuidMap1.entrySet()) {
if (entry.getKey().equalsIgnoreCase(player)) {
persistInfo(entry.getKey(), entry.getValue(), jedis);
return entry.getValue();
}
}
} catch (JedisException e) {
plugin.logFatal("Unable to fetch UUID for " + player);
}
return null; // Nope, game over!
}
public final String getNameFromUuid(@NonNull UUID player, boolean expensiveLookups) {
// If the player is online, give them their UUID.
// Remember, local data > remote data.
if (plugin.getPlayer(player) != null)
return plugin.getPlayerName(player);
// Check if it exists in the map
CachedUUIDEntry cachedUUIDEntry = uuidToNameMap.get(player);
if (cachedUUIDEntry != null) {
if (!cachedUUIDEntry.expired())
return cachedUUIDEntry.getName();
else
uuidToNameMap.remove(player);
}
// Okay, it wasn't locally cached. Let's try Redis.
try (Jedis jedis = plugin.requestJedis()) {
String stored = jedis.hget("uuid-cache", player.toString());
if (stored != null) {
// Found an entry value. Deserialize it.
CachedUUIDEntry entry = gson.fromJson(stored, CachedUUIDEntry.class);
// Check for expiry:
if (entry.expired()) {
jedis.hdel("uuid-cache", player.toString());
// Doesn't hurt to also remove the named entry as well.
// TODO: Since UUIDs are fixed, we could look up the name and see if the UUID matches.
jedis.hdel("uuid-cache", entry.getName());
} else {
nameToUuidMap.put(entry.getName().toLowerCase(), entry);
uuidToNameMap.put(player, entry);
return entry.getName();
}
}
if (!expensiveLookups || !plugin.isOnlineMode())
return null;
// That didn't work. Let's ask Mojang. This call may fail, because Mojang is insane.
String name;
try {
List<String> nameHist = NameFetcher.nameHistoryFromUuid(player);
name = Iterables.getLast(nameHist, null);
} catch (Exception e) {
plugin.logFatal("Unable to fetch name from Mojang for " + player);
return null;
}
if (name != null) {
persistInfo(name, player, jedis);
return name;
}
return null;
} catch (JedisException e) {
plugin.logFatal("Unable to fetch name for " + player);
return null;
}
}
public final void persistInfo(String name, UUID uuid, Jedis jedis) {
addToMaps(name, uuid);
String json = gson.toJson(uuidToNameMap.get(uuid));
jedis.hmset("uuid-cache", ImmutableMap.of(name.toLowerCase(), json, uuid.toString(), json));
}
public final void persistInfo(String name, UUID uuid, Pipeline jedis) {
addToMaps(name, uuid);
String json = gson.toJson(uuidToNameMap.get(uuid));
jedis.hmset("uuid-cache", ImmutableMap.of(name.toLowerCase(), json, uuid.toString(), json));
}
private static class CachedUUIDEntry {
private final String name;
private final UUID uuid;
private final Calendar expiry;
public CachedUUIDEntry(String name, UUID uuid, Calendar expiry) {
this.name = name;
this.uuid = uuid;
this.expiry = expiry;
}
public String getName() {
return name;
}
public UUID getUuid() {
return uuid;
}
public Calendar getExpiry() {
return expiry;
}
public boolean expired() {
return Calendar.getInstance().after(expiry);
}
}
}