ProjectAres/Commons/bukkit/src/main/java/tc/oc/commons/bukkit/listeners/LoginListener.java

253 lines
10 KiB
Java

package tc.oc.commons.bukkit.listeners;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventBus;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.plugin.Plugin;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.UserDoc;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.users.LoginRequest;
import tc.oc.api.users.LoginResponse;
import tc.oc.api.users.UserService;
import tc.oc.api.util.Permissions;
import tc.oc.commons.bukkit.chat.ComponentRenderContext;
import tc.oc.commons.bukkit.event.AsyncUserLoginEvent;
import tc.oc.commons.bukkit.event.UserLoginEvent;
import tc.oc.commons.bukkit.punishment.PunishmentFormatter;
import tc.oc.commons.bukkit.util.PermissionUtils;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.concurrent.Locker;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.minecraft.api.scheduler.Scheduler;
import tc.oc.minecraft.protocol.MinecraftVersion;
@Singleton
public class LoginListener implements Listener, PluginFacet {
private static final String INTERNAL_SERVER_ERROR = "Sorry, but there was an internal server error.\n" +
"We are working to resolve the issue: please check back soon.";
private static final String SERVER_IS_RESTARTING = "Server is restarting, please reconnect in a moment";
private final Logger logger;
private final Plugin plugin;
private final EventBus eventBus;
private final Scheduler scheduler;
private final MinecraftService minecraftService;
private final UserService userService;
private final BukkitUserStore userStore;
private final ComponentRenderContext renderer;
private final PunishmentFormatter punishmentFormatter;
private boolean connected;
private final ReadWriteLock connectedLock = new ReentrantReadWriteLock();
// MC login times out in 30 seconds so caching for 1 minute should be fine
private final Cache<UUID, LoginResponse> logins = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Inject LoginListener(Loggers loggers, Plugin plugin, EventBus eventBus, Scheduler scheduler, MinecraftService minecraftService, UserService userService, BukkitUserStore userStore, ComponentRenderContext renderer, PunishmentFormatter punishmentFormatter) {
this.eventBus = eventBus;
this.logger = loggers.get(getClass());
this.scheduler = scheduler;
this.minecraftService = minecraftService;
this.userService = userService;
this.userStore = userStore;
this.plugin = plugin;
this.renderer = renderer;
this.punishmentFormatter = punishmentFormatter;
}
@Override
public void enable() {
try(Locker _ = Locker.lock(connectedLock.writeLock())) {
connected = true;
}
}
@Override
public void disable() {
try(Locker _ = Locker.lock(connectedLock.writeLock())) {
connected = false;
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void preLogin(final AsyncPlayerPreLoginEvent event) {
this.logger.info(event.getName() + " pre-login: uuid=" + event.getUniqueId() + " ip=" + event.getAddress());
try(Locker _ = Locker.lock(connectedLock.readLock())) {
this.logins.invalidate(event.getUniqueId());
if(!connected) {
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, SERVER_IS_RESTARTING);
return;
}
LoginResponse response = this.userService.login(
new LoginRequest(event.getName(),
event.getUniqueId(),
event.getAddress(),
minecraftService.getLocalServer(),
true)
).get();
if(response.kick() != null) switch(response.kick()) {
case "error":
this.logger.info(event.getName() + " login error: " + response.message());
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, response.message());
break;
case "banned": // Only used for IP bans right now
this.logger.info(event.getName() + " is banned");
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, response.message());
break;
}
if(event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) return;
this.logins.put(event.getUniqueId(), response);
eventBus.callEvent(new AsyncUserLoginEvent(response));
} catch(Exception e) {
this.logger.log(Level.SEVERE, e.toString(), e);
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, INTERNAL_SERVER_ERROR);
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void login(PlayerLoginEvent event) {
try {
final Player player = event.getPlayer();
final UUID uuid = player.getUniqueId();
player.setGravity(true);
this.logins.cleanUp();
final LoginResponse response = this.logins.getIfPresent(uuid);
this.logins.invalidate(uuid);
if(response == null) {
this.logger.warning("No login info for " + player.getName() + " " + uuid);
event.disallow(PlayerLoginEvent.Result.KICK_OTHER, INTERNAL_SERVER_ERROR);
return;
}
// TODO: Consider creating a PreUserLoginEvent that can be cancelled,
// before things like sessions are started.
userStore.addUser(player, response.user());
applyPermissions(player, response.user());
if(response.punishment() != null) {
rejectLogin(event, punishmentFormatter.screen(response.punishment()));
}
if(!player.hasPermission(Permissions.LOGIN)) {
rejectLogin(event, new TranslatableComponent("servers.notAllowed"));
}
if(event.getResult() == PlayerLoginEvent.Result.KICK_FULL) {
// Allow privileged players to join when the server is full
if(player.hasPermission("pgm.fullserver")) {
event.allow();
} else {
rejectLogin(event, new TranslatableComponent("serverFull"));
}
}
if(response.user().mc_locale() != null) {
// If we have a saved locale for the player, apply it.
// This should ensure that text displayed on join is properly
// localized, as long as the player has connected once before.
player.setLocale(response.user().mc_locale());
}
userService.update(response.user(), new UserDoc.ClientDetails() {
@Override public String mc_client_version() {
return MinecraftVersion.describeProtocol(player.getProtocolVersion());
}
@Override public String skin_blob() {
return player.getSkin().getData();
}
});
if(event.getResult() == PlayerLoginEvent.Result.KICK_OTHER) return;
final UserLoginEvent ourEvent = new UserLoginEvent(
player, response, event.getResult(),
event.getKickMessage() == null || "".equals(event.getKickMessage()) ? null : new Component(event.getKickMessage())
);
eventBus.callEvent(ourEvent);
event.setResult(ourEvent.getResult());
event.setKickMessage(ourEvent.getKickMessage() == null ? "" : renderer.renderLegacy(ourEvent.getKickMessage(), player));
}
catch(Exception e) {
this.logger.log(Level.SEVERE, e.toString(), e);
event.disallow(PlayerLoginEvent.Result.KICK_OTHER, INTERNAL_SERVER_ERROR);
}
}
protected void applyPermissions(Player player, UserDoc.Login userDoc) {
boolean op = false;
final Server localServer = minecraftService.getLocalServer();
if(localServer.operators().containsKey(player.getUniqueId())) {
logger.info("Opping " + player.getName() + " because they are in the server op list");
op = true;
}
if(localServer.team() != null && localServer.team().members().contains(userDoc)) {
logger.info("Opping " + player.getName() + " because they are on the team that owns the server");
op = true;
}
PermissionAttachment attachment = player.addAttachment(this.plugin);
PermissionUtils.setPermissions(attachment, Permissions.mergePermissions(localServer.realms(), userDoc.mc_permissions_by_realm()));
player.recalculatePermissions();
if(player.hasPermission("op")) {
op = true;
logger.info("Opping " + player.getName() + " because they have the op permission node");
}
player.setOp(op); // This is always explicitly set to true or false on login
}
protected void rejectLogin(PlayerLoginEvent event, BaseComponent message) {
if(event.getResult() != PlayerLoginEvent.Result.KICK_OTHER) {
event.disallow(PlayerLoginEvent.Result.KICK_OTHER, renderer.renderLegacy(message, event.getPlayer()));
}
}
@EventHandler
private void quit(PlayerQuitEvent event) {
scheduler.runSync(() -> userStore.removeUser(event.getPlayer()));
}
}