253 lines
10 KiB
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()));
|
|
}
|
|
}
|