public class CrackedCommand extends ToggleCommand {

    public CrackedCommand(FastLoginBukkit plugin) {
        super(plugin);
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
        if (args.length == 0) {
            onCrackedSelf(sender, command, args);
        } else {
            onCrackedOther(sender, command, args);
        }

        return true;
    }

    private void onCrackedSelf(CommandSender sender, Command cmd, String[] args) {
        if (isConsole(sender)) {
            return;
        }

        if (forwardCrackedCommand(sender, sender.getName())) {
            return;
        }

        if (plugin.getBungeeManager().isEnabled()) {
            sendBungeeActivateMessage(sender, sender.getName(), false);
            plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
        } else {
            //todo: load async if
            StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
            if (profile.isPremium()) {
                plugin.getCore().sendLocaleMessage("remove-premium", sender);

                profile.setPremium(false);
                profile.setId(null);
                plugin.getScheduler().runAsync(() -> {
                    plugin.getCore().getStorage().save(profile);
                    plugin.getServer().getPluginManager().callEvent(
                            new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
                });
            } else {
                plugin.getCore().sendLocaleMessage("not-premium", sender);
            }
        }
    }

    private void onCrackedOther(CommandSender sender, Command command, String[] args) {
        if (!hasOtherPermission(sender, command)) {
            return;
        }

        if (forwardCrackedCommand(sender, args[0])) {
            return;
        }

        //todo: load async
        StoredProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
        if (profile == null) {
            sender.sendMessage("Error occurred");
            return;
        }

        //existing player is already cracked
        if (profile.isSaved() && !profile.isPremium()) {
            plugin.getCore().sendLocaleMessage("not-premium-other", sender);
        } else {
            plugin.getCore().sendLocaleMessage("remove-premium", sender);

            profile.setPremium(false);
            plugin.getScheduler().runAsync(() -> {
                plugin.getCore().getStorage().save(profile);
                plugin.getServer().getPluginManager().callEvent(
                        new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
            });
        }
    }

    private boolean forwardCrackedCommand(CommandSender sender, String target) {
        return forwardBungeeCommand(sender, target, false);
    }
} public class PremiumCommand extends ToggleCommand {

    public PremiumCommand(FastLoginBukkit plugin) {
        super(plugin);
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
        if (args.length == 0) {
            onPremiumSelf(sender, command, args);
        } else {
            onPremiumOther(sender, command, args);
        }

        return true;
    }

    private void onPremiumSelf(CommandSender sender, Command cmd, String[] args) {
        if (isConsole(sender)) {
            return;
        }

        if (forwardPremiumCommand(sender, sender.getName())) {
            return;
        }

        UUID id = ((Player) sender).getUniqueId();
        if (plugin.getConfig().getBoolean("premium-warning") && !plugin.getCore().getPendingConfirms().contains(id)) {
            sender.sendMessage(plugin.getCore().getMessage("premium-warning"));
            plugin.getCore().getPendingConfirms().add(id);
            return;
        }

        plugin.getCore().getPendingConfirms().remove(id);
        //todo: load async
        StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
        if (profile.isPremium()) {
            plugin.getCore().sendLocaleMessage("already-exists", sender);
        } else {
            //todo: resolve uuid
            profile.setPremium(true);
            plugin.getScheduler().runAsync(() -> {
                plugin.getCore().getStorage().save(profile);
                plugin.getServer().getPluginManager().callEvent(
                        new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_SELF));
            });

            plugin.getCore().sendLocaleMessage("add-premium", sender);
        }
    }

    private void onPremiumOther(CommandSender sender, Command command, String[] args) {
        if (!hasOtherPermission(sender, command)) {
            return;
        }

        if (forwardPremiumCommand(sender, args[0])) {
            return;
        }

        //todo: load async
        StoredProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
        if (profile == null) {
            plugin.getCore().sendLocaleMessage("player-unknown", sender);
            return;
        }

        if (profile.isPremium()) {
            plugin.getCore().sendLocaleMessage("already-exists-other", sender);
        } else {
            //todo: resolve uuid
            profile.setPremium(true);
            plugin.getScheduler().runAsync(() -> {
                plugin.getCore().getStorage().save(profile);
                plugin.getServer().getPluginManager().callEvent(
                        new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
            });

            plugin.getCore().sendLocaleMessage("add-premium-other", sender);
        }
    }

    private boolean forwardPremiumCommand(CommandSender sender, String target) {
        return forwardBungeeCommand(sender, target, true);
    }
} public class ConnectionListener implements Listener {

    private static final long DELAY_LOGIN = 20L / 2;

    private final FastLoginBukkit plugin;

    public ConnectionListener(FastLoginBukkit plugin) {
        this.plugin = plugin;
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onPlayerLogin(PlayerLoginEvent loginEvent) {
        removeBlockedStatus(loginEvent.getPlayer());
        if (loginEvent.getResult() == Result.ALLOWED && !plugin.isServerFullyStarted()) {
            loginEvent.disallow(Result.KICK_OTHER, plugin.getCore().getMessage("not-started"));
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerJoin(PlayerJoinEvent joinEvent) {
        Player player = joinEvent.getPlayer();

        Bukkit.getScheduler().runTaskLater(plugin, () -> {
            delayForceLogin(player);
            // delay the login process to let auth plugins initialize the player
            // Magic number however as there is no direct event from those plugins
        }, DELAY_LOGIN);
    }

    private void delayForceLogin(Player player) {
        // session exists so the player is ready for force login
        // cases: Paper (firing BungeeCord message before PlayerJoinEvent) or not running BungeeCord and already
        // having the login session from the login process
        BukkitLoginSession session = plugin.getSession(player.getAddress());

        if (session == null) {
            // Floodgate players usually don't have a session at this point
            // exception: if force login by bungee message had been delayed
            FloodgateService floodgateService = plugin.getFloodgateService();
            if (floodgateService != null) {
                FloodgatePlayer floodgatePlayer = floodgateService.getBedrockPlayer(player.getUniqueId());
                if (floodgatePlayer != null) {
                    Runnable floodgateAuthTask = new FloodgateAuthTask(plugin.getCore(), player, floodgatePlayer);
                    Bukkit.getScheduler().runTaskAsynchronously(plugin, floodgateAuthTask);
                    return;
                }
            }

            String sessionId = plugin.getSessionId(player.getAddress());
            plugin.getLog().info("No on-going login session for player: {} with ID {}", player, sessionId);
        } else {
            Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player, session);
            Bukkit.getScheduler().runTaskAsynchronously(plugin, forceLoginTask);
        }

        plugin.getBungeeManager().markJoinEventFired(player);
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent quitEvent) {
        Player player = quitEvent.getPlayer();

        removeBlockedStatus(player);
        plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
        plugin.getPremiumPlayers().remove(player.getUniqueId());
        plugin.getBungeeManager().cleanup(player);
    }

    private void removeBlockedStatus(Metadatable player) {
        player.removeMetadata(plugin.getName(), plugin);
    }
} public class PaperCacheListener implements Listener {

    private final FastLoginBukkit plugin;

    public PaperCacheListener(final FastLoginBukkit plugin) {
        this.plugin = plugin;
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    //if paper is used - player skin must be set at pre login, otherwise user cache is used
    // user cache makes premium name change basically impossible
    public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
        if (event.getLoginResult() != Result.ALLOWED) {
            return;
        }

        // event gives us only IP, not the port, so we need to loop through all the sessions
        for (BukkitLoginSession session : plugin.getLoginSessions().values()) {
            if (!event.getName().equals(session.getUsername())) {
                continue;
            }

            session.getSkin().ifPresent(skin -> event.getPlayerProfile().setProperty(new ProfileProperty(Textures.KEY,
                    skin.getValue(), skin.getSignature())));
            break;
        }
    }

} public class ManualNameChange extends PacketAdapter {

    private final FloodgateService floodgate;

    public ManualNameChange(FastLoginBukkit plugin, FloodgateService floodgate) {
        super(params()
                .plugin(plugin)
                .types(START));

        this.plugin = plugin;
        this.floodgate = floodgate;
    }

    public static void register(FastLoginBukkit plugin, FloodgateService floodgate) {
        // they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
        ProtocolLibrary.getProtocolManager()
                .getAsynchronousManager()
                .registerAsyncHandler(new ManualNameChange(plugin, floodgate))
                .start();
    }

    @Override
    public void onPacketReceiving(PacketEvent packetEvent) {
        PacketContainer packet = packetEvent.getPacket();
        WrappedGameProfile originalProfile = packet.getGameProfiles().read(0);

        if (floodgate.getBedrockPlayer(originalProfile.getName()) == null) {
            //not a Floodgate player, no need to add a prefix
            return;
        }

        packet.setMeta("original_name", originalProfile.getName());
        String prefixedName = FloodgateApi.getInstance().getPlayerPrefix() + originalProfile.getName();
        WrappedGameProfile updatedProfile = originalProfile.withName(prefixedName);
        packet.getGameProfiles().write(0, updatedProfile);
    }
} public class ProtocolLibListener extends PacketAdapter {

    public static final String SOURCE_META_KEY = "source";

    private final FastLoginBukkit plugin;

    //just create a new once on plugin enable. This used for verify token generation
    private final SecureRandom random = new SecureRandom();
    private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
    private final RateLimiter rateLimiter;

    public ProtocolLibListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
        //run async in order to not block the server, because we are making api calls to Mojang
        super(params()
                .plugin(plugin)
                .types(START, ENCRYPTION_BEGIN)
                .optionAsync());

        this.plugin = plugin;
        this.rateLimiter = rateLimiter;
    }

    public static void register(FastLoginBukkit plugin, RateLimiter rateLimiter) {
        // they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
        // TODO: make synchronous processing, but do web or database requests async
        ProtocolLibrary.getProtocolManager()
                .getAsynchronousManager()
                .registerAsyncHandler(new ProtocolLibListener(plugin, rateLimiter))
                .start();
    }

    @Override
    public void onPacketReceiving(PacketEvent packetEvent) {
        if (packetEvent.isCancelled()
                || plugin.getCore().getAuthPluginHook() == null
                || !plugin.isServerFullyStarted()) {
            return;
        }

        if (isFastLoginPacket(packetEvent)) {
            // this is our own packet
            return;
        }

        Player sender = packetEvent.getPlayer();
        PacketType packetType = packetEvent.getPacketType();
        if (packetType == START) {
            if (!rateLimiter.tryAcquire()) {
                plugin.getLog().warn("Simple Anti-Bot join limit - Ignoring {}", sender);
                return;
            }

            onLogin(packetEvent, sender);
        } else {
            onEncryptionBegin(packetEvent, sender);
        }
    }

    private Boolean isFastLoginPacket(PacketEvent packetEvent) {
        return packetEvent.getPacket().getMeta(SOURCE_META_KEY)
                .map(val -> val.equals(plugin.getName()))
                .orElse(false);
    }

    private void onEncryptionBegin(PacketEvent packetEvent, Player sender) {
        byte[] sharedSecret = packetEvent.getPacket().getByteArrays().read(0);

        packetEvent.getAsyncMarker().incrementProcessingDelay();
        Runnable verifyTask = new VerifyResponseTask(plugin, packetEvent, sender, sharedSecret, keyPair);
        plugin.getScheduler().runAsync(verifyTask);
    }

    private void onLogin(PacketEvent packetEvent, Player player) {
        //this includes ip:port. Should be unique for an incoming login request with a timeout of 2 minutes
        String sessionKey = player.getAddress().toString();

        //remove old data every time on a new login in order to keep the session only for one person
        plugin.removeSession(player.getAddress());

        //player.getName() won't work at this state
        PacketContainer packet = packetEvent.getPacket();

        String username = packet.getGameProfiles().read(0).getName();

        if (packetEvent.getPacket().getMeta("original_name").isPresent()) {
            //username has been injected by ManualNameChange.java
            username = (String) packetEvent.getPacket().getMeta("original_name").get();
        }

        plugin.getLog().trace("GameProfile {} with {} connecting", sessionKey, username);

        packetEvent.getAsyncMarker().incrementProcessingDelay();
        Runnable nameCheckTask = new NameCheckTask(plugin, random, player, packetEvent, username, keyPair.getPublic());
        plugin.getScheduler().runAsync(nameCheckTask);
    }
} public class SkinApplyListener implements Listener {

    private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
    private static final MethodAccessor GET_PROPERTIES = Accessors.getMethodAccessor(GAME_PROFILE, "getProperties");

    private final FastLoginBukkit plugin;

    public SkinApplyListener(FastLoginBukkit plugin) {
        this.plugin = plugin;
    }

    @EventHandler(priority = EventPriority.LOW)
    //run this on the loginEvent to let skins plugins see the skin like in normal Minecraft behaviour
    public void onPlayerLogin(PlayerLoginEvent loginEvent) {
        if (loginEvent.getResult() != Result.ALLOWED) {
            return;
        }

        Player player = loginEvent.getPlayer();

        //go through every session, because player.getAddress is null
        //loginEvent.getAddress is just a InetAddress not InetSocketAddress, so not unique enough
        for (BukkitLoginSession session : plugin.getLoginSessions().values()) {
            if (session.getUsername().equals(player.getName())) {
                session.getSkin().ifPresent(skin -> applySkin(player, skin.getValue(), skin.getSignature()));
                break;
            }
        }
    }

    private void applySkin(Player player, String skinData, String signature) {
        WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);

        WrappedSignedProperty skin = WrappedSignedProperty.fromValues(Textures.KEY, skinData, signature);
        try {
            gameProfile.getProperties().put(Textures.KEY, skin);
        } catch (ClassCastException castException) {
            //Cauldron, MCPC, Thermos, ...
            Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
            try {
                MethodUtils.invokeMethod(map, "put", new Object[]{Textures.KEY, skin.getHandle()});
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
                plugin.getLog().error("Error setting premium skin of: {}", player, ex);
            }
        }
    }
} public class CommonUtil {

    private static final char COLOR_CHAR = '&';
    private static final char TRANSLATED_CHAR = '§';

    public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
        SafeCacheBuilder<Object, Object> builder = SafeCacheBuilder.newBuilder();

        if (expireAfterWrite > 0) {
            builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);
        }

        if (maxSize > 0) {
            builder.maximumSize(maxSize);
        }

        return builder.build(CacheLoader.from(() -> {
            throw new UnsupportedOperationException();
        }));
    }

    public static String translateColorCodes(String rawMessage) {
        char[] chars = rawMessage.toCharArray();
        for (int i = 0; i < chars.length - 1; i++) {
            if (chars[i] == COLOR_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(chars[i + 1]) > -1) {
                chars[i] = TRANSLATED_CHAR;
                chars[i + 1] = Character.toLowerCase(chars[i + 1]);
            }
        }

        return new String(chars);
    }

    /**
     * This creates a SLF4J logger. In the process it initializes the SLF4J service provider. This method looks
     * for the provider in the plugin jar instead of in the server jar when creating a Logger. The provider is only
     * initialized once, so this method should be called early.
     *
     * The provider is bound to the service class `SLF4JServiceProvider`. Relocating this class makes it available
     * for exclusive own usage. Other dependencies will use the relocated service too, and therefore will find the
     * initialized provider.
     *
     * @param parent JDK logger
     * @return slf4j logger
     */
    public static Logger initializeLoggerService(java.util.logging.Logger parent) {
        // set the class loader to the plugin one to find our own SLF4J provider in the plugin jar and not in the global
        ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();

        ClassLoader pluginLoader = CommonUtil.class.getClassLoader();
        Thread.currentThread().setContextClassLoader(pluginLoader);

        // Trigger provider search
        LoggerFactory.getLogger(parent.getName()).info("Initialize logging service");
        try {
            parent.setLevel(Level.ALL);

            Class<JDK14LoggerAdapter> adapterClass = JDK14LoggerAdapter.class;
            Constructor<JDK14LoggerAdapter> cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class);
            cons.setAccessible(true);
            return cons.newInstance(parent);
        } catch (ReflectiveOperationException reflectEx) {
            parent.log(Level.WARNING, "Cannot create slf4j logging adapter", reflectEx);
            parent.log(Level.WARNING, "Creating logger instance manually...");
            return LoggerFactory.getLogger(parent.getName());
        } finally {
            // restore previous class loader
            Thread.currentThread().setContextClassLoader(oldLoader);
        }
    }

    private CommonUtil() {
        //Utility class
    }
} public abstract class BedrockService<B> {

    protected final FastLoginCore<?, ?, ?> core;
    protected final String allowConflict;

    public BedrockService(FastLoginCore<?, ?, ?> core) {
        this.core = core;
        this.allowConflict = core.getConfig().get("allowFloodgateNameConflict").toString().toLowerCase();
    }

    /**
     * Perform every packet level check needed on a Bedrock player.
     *
     * @param username the name of the player
     * @param source   an instance of LoginSource
     * @return true if Java specific checks can be skipped
     */
    public abstract boolean performChecks(String username, LoginSource source);

    /**
     * Check if the player's name conflicts an existing Java player's name, and kick
     * them if it does
     *
     * @param username the name of the player
     * @param source   an instance of LoginSource
     */
    protected void checkNameConflict(String username, LoginSource source) {
        // check for conflicting Premium Java name
        Optional<Profile> premiumUUID = Optional.empty();
        try {
            premiumUUID = core.getResolver().findProfile(username);
        } catch (IOException ioEx) {
            core.getPlugin().getLog().error(
                "Could not check whether Bedrock Player {}'s name conflicts a premium Java player's name.",
                username);

            kickPlayer(source, username, "Could not check if your name conflicts an existing " +
                "premium Java account's name. This is usually a serverside error.");
        } catch (RateLimitException rateLimitException) {
            core.getPlugin().getLog().warn("Mojang API rate limit hit");
            kickPlayer(source, username, "Could not check if your name conflicts an existing premium " +
                "Java account's name. Try again in a few minutes");
        }

        if (premiumUUID.isPresent()) {
            core.getPlugin().getLog().info("Bedrock Player {}'s name conflicts an existing premium Java account's name",
                    username);
            kickPlayer(source, username, "Your name conflicts an existing premium Java account's name");
        }
    }

    private void kickPlayer(LoginSource source, String username, String message) {
        try {
            source.kick(message);
        } catch (Exception ex) {
            core.getPlugin().getLog().error("Could not kick Player {}", username, ex);
        }
    }

    /**
     * The Floodgate / Geyser API does not support querying players by name, so this function
     * iterates over every online Bedrock Player and checks if the requested
     * username can be found
     * <br>
     * <i>Falls back to non-prefixed name checks, if ProtocolLib is installed</i>
     *
     * @param prefixedUsername the name of the player with the prefix appended
     * @return Bedrock Player if found, null otherwise
     */
    public B getBedrockPlayer(String prefixedUsername) {
        return null;
    }

    public B getBedrockPlayer(UUID uuid) {
        return null;
    }

    public boolean isBedrockPlayer(UUID uuid) {
        return getBedrockPlayer(uuid) != null;
    }

    public boolean isBedrockConnection(String username) {
        return getBedrockPlayer(username) != null;
    }

    /**
     * Checks if a profile's name starts with the Floodgate prefix, if it's available
     * @param profile profile of the connecting player
     * @return true if the username is forbidden
     */
    public boolean isUsernameForbidden(StoredProfile profile) {
        return false;
    }

} public class GeyserService extends BedrockService<GeyserSession> {

    private final GeyserImpl geyser;
    private final FastLoginCore<?, ?, ?> core;
    private final AuthType authType;

    public GeyserService(GeyserImpl geyser, FastLoginCore<?, ?, ?> core) {
        super(core);
        this.geyser = geyser;
        this.core = core;
        this.authType = GeyserImpl.getInstance().getConfig().getRemote().getAuthType();
    }

    @Override
    public boolean performChecks(String username, LoginSource source) {
        // AuthType.FLOODGATE will be handled by FloodgateService
        if (authType == AuthType.ONLINE) {
            // authenticate everyone, as if they were Java players, since they have signed
            // in through Mojang
            return false;
        }
        if ("true".equals(allowConflict)) {
            core.getPlugin().getLog().info("Skipping name conflict checking for player {}", username);
        } else {
            super.checkNameConflict(username, source);
        }
        return true;
    }

    @Override
    public GeyserSession getBedrockPlayer(String username) {
        return geyser.connectionByName(username);
    }

    @Override
    public GeyserSession getBedrockPlayer(UUID uuid) {
        return geyser.connectionByUuid(uuid);
    }
} public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {

    private static final long MAX_EXPIRE_RATE = 1_000_000;

    private final Map<String, String> localeMessages = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Object> pendingLogin = CommonUtil.buildCache(5, -1);
    private final Collection<UUID> pendingConfirms = new HashSet<>();
    private final T plugin;

    private final MojangResolver resolver = new MojangResolver();

    private Configuration config;
    private SQLStorage storage;
    private RateLimiter rateLimiter;
    private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
    private AuthPlugin<P> authPlugin;

    public FastLoginCore(T plugin) {
        this.plugin = plugin;
    }

    public void load() {
        saveDefaultFile("messages.yml");
        saveDefaultFile("config.yml");

        try {
            config = loadFile("config.yml");
            Configuration messages = loadFile("messages.yml");

            messages.getKeys()
                    .stream()
                    .filter(key -> messages.get(key) != null)
                    .collect(toMap(identity(), messages::get))
                    .forEach((key, message) -> {
                        String colored = CommonUtil.translateColorCodes((String) message);
                        if (!colored.isEmpty()) {
                            localeMessages.put(key, colored.replace("/newline", "\n"));
                        }
                    });
        } catch (IOException ioEx) {
            plugin.getLog().error("Failed to load yaml files", ioEx);
            return;
        }

        rateLimiter = createRateLimiter(config.getSection("anti-bot"));
        Set<Proxy> proxies = config.getStringList("proxies")
                .stream()
                .map(HostAndPort::fromString)
                .map(proxy -> new InetSocketAddress(proxy.getHost(), proxy.getPort()))
                .map(sa -> new Proxy(Type.HTTP, sa))
                .collect(toSet());

        Collection<InetAddress> addresses = new HashSet<>();
        for (String localAddress : config.getStringList("ip-addresses")) {
            try {
                addresses.add(InetAddress.getByName(localAddress.replace('-', '.')));
            } catch (UnknownHostException ex) {
                plugin.getLog().error("IP-Address is unknown to us", ex);
            }
        }

        resolver.setMaxNameRequests(config.getInt("mojang-request-limit"));
        resolver.setProxySelector(new RotatingProxySelector(proxies));
        resolver.setOutgoingAddresses(addresses);
    }

    private RateLimiter createRateLimiter(Configuration botSection) {
        boolean enabled = botSection.getBoolean("enabled", true);
        if (!enabled) {
            // no-op rate limiter
            return () -> true;
        }

        int maxCon = botSection.getInt("anti-bot.connections", 200);
        long expireTime = botSection.getLong("anti-bot.expire", 5) * 60 * 1_000L;
        if (expireTime > MAX_EXPIRE_RATE) {
            expireTime = MAX_EXPIRE_RATE;
        }

        return new TickingRateLimiter(Ticker.systemTicker(), maxCon, expireTime);
    }

    private Configuration loadFile(String fileName) throws IOException {
        ConfigurationProvider configProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);

        Configuration defaults;
        try (InputStream defaultStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
            defaults = configProvider.load(defaultStream);
        }

        Path file = plugin.getPluginFolder().resolve(fileName);

        Configuration config;
        try (Reader reader = Files.newBufferedReader(file)) {
            config = configProvider.load(reader, defaults);
        }

        // explicitly add keys here, because Configuration.getKeys doesn't return the keys from the default configuration
        for (String key : defaults.getKeys()) {
            config.set(key, config.get(key));
        }

        return config;
    }

    public MojangResolver getResolver() {
        return resolver;
    }

    public SQLStorage getStorage() {
        return storage;
    }

    public T getPlugin() {
        return plugin;
    }

    public void sendLocaleMessage(String key, C receiver) {
        String message = localeMessages.get(key);
        if (message != null) {
            plugin.sendMultiLineMessage(receiver, message);
        }
    }

    public String getMessage(String key) {
        return localeMessages.get(key);
    }

    public boolean setupDatabase() {
        String driver = config.getString("driver");
        if (!checkDriver(driver)) {
            return false;
        }

        HikariConfig databaseConfig = new HikariConfig();
        databaseConfig.setDriverClassName(driver);

        String database = config.getString("database");

        databaseConfig.setConnectionTimeout(config.getInt("timeout", 30) * 1_000L);
        databaseConfig.setMaxLifetime(config.getInt("lifetime", 30) * 1_000L);

        if (driver.contains("sqlite")) {
            storage = new SQLiteStorage(this, database, databaseConfig);
        } else {
            String host = config.get("host", "");
            int port = config.get("port", 3306);
            boolean useSSL = config.get("useSSL", false);

            if (useSSL) {
                databaseConfig.addDataSourceProperty("allowPublicKeyRetrieval", config.getBoolean("allowPublicKeyRetrieval", false));
                databaseConfig.addDataSourceProperty("serverRSAPublicKeyFile", config.getString("ServerRSAPublicKeyFile"));
                databaseConfig.addDataSourceProperty("sslMode", config.getString("sslMode", "Required"));
            }

            databaseConfig.setUsername(config.get("username", ""));
            databaseConfig.setPassword(config.getString("password"));
            storage = new MySQLStorage(this, driver, host, port, database, databaseConfig, useSSL);
        }

        try {
            storage.createTables();
            return true;
        } catch (Exception ex) {
            plugin.getLog().warn("Failed to setup database. Disabling plugin...", ex);
            return false;
        }
    }

    private boolean checkDriver(String className) {
        try {
            Class.forName(className);
            return true;
        } catch (ClassNotFoundException notFoundEx) {
            Logger log = plugin.getLog();
            log.warn("This driver {} is not supported on this platform", className);
            log.warn("Please choose either MySQL (Spigot, BungeeCord), SQLite (Spigot, Sponge) or " +
                "MariaDB (Sponge, Velocity)", notFoundEx);
        }

        return false;
    }

    public Configuration getConfig() {
        return config;
    }

    public PasswordGenerator<P> getPasswordGenerator() {
        return passwordGenerator;
    }

    public void setPasswordGenerator(PasswordGenerator<P> passwordGenerator) {
        this.passwordGenerator = passwordGenerator;
    }

    public ConcurrentMap<String, Object> getPendingLogin() {
        return pendingLogin;
    }

    public Collection<UUID> getPendingConfirms() {
        return pendingConfirms;
    }

    public AuthPlugin<P> getAuthPluginHook() {
        return authPlugin;
    }

    public RateLimiter getRateLimiter() {
        return rateLimiter;
    }

    public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
        this.authPlugin = authPlugin;
    }

    public void saveDefaultFile(String fileName) {
        Path dataFolder = plugin.getPluginFolder();

        try {
            Files.createDirectories(dataFolder);

            Path configFile = dataFolder.resolve(fileName);
            if (Files.notExists(configFile)) {
                try (InputStream defaultStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
                    Files.copy(Objects.requireNonNull(defaultStream), configFile);
                }
            }
        } catch (IOException ioExc) {
            plugin.getLog().error("Cannot create plugin folder {}", dataFolder, ioExc);
        }
    }

    public void close() {
        plugin.getLog().info("Safely shutting down scheduler. This could take up to one minute.");
        plugin.getScheduler().shutdown();

        if (storage != null) {
            storage.close();
        }
    }
} public interface PlatformPlugin<C> {

    String getName();

    Path getPluginFolder();

    Logger getLog();

    void sendMessage(C receiver, String message);

    AsyncScheduler getScheduler();

    boolean isPluginInstalled(String name);

    default void sendMultiLineMessage(C receiver, String message) {
        for (String line : message.split("%nl%")) {
            sendMessage(receiver, line);
        }
    }

    BedrockService<?> getBedrockService();

    default ThreadFactory getThreadFactory() {
        return new ThreadFactoryBuilder()
                .setNameFormat(getName() + " Pool Thread #%1$d")
                // Hikari create daemons by default and we could daemon threads for our own scheduler too
                // because we safely shutdown
                .setDaemon(true)
                .build();
    }
}
import com.github.games647.fastlogin.bukkit.command.CrackedCommand;
import com.github.games647.fastlogin.bukkit.command.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.ConnectionListener;
import com.github.games647.fastlogin.bukkit.listener.PaperCacheListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ManualNameChange;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ProtocolLibListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SkinApplyListener;
import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSupportListener;
import com.github.games647.fastlogin.bukkit.task.DelayedAuthHook;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.PremiumStatus;
import com.github.games647.fastlogin.core.hooks.bedrock.BedrockService;
import com.github.games647.fastlogin.core.hooks.bedrock.FloodgateService;
import com.github.games647.fastlogin.core.hooks.bedrock.GeyserService;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import io.papermc.lib.PaperLib;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.geyser.GeyserImpl;
import org.slf4j.Logger;
/*
 * SPDX-License-Identifier: MIT
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2021 <Your name and contributors>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.github.games647.fastlogin.bukkit;





/**
 * This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
 */
public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<CommandSender> {

    //1 minutes should be enough as a timeout for bad internet connection (Server, Client and Mojang)
    private final ConcurrentMap<String, BukkitLoginSession> loginSession = CommonUtil.buildCache(1, -1);
    private final Map<UUID, PremiumStatus> premiumPlayers = new ConcurrentHashMap<>();
    private final Logger logger;

    private boolean serverStarted;
    private BungeeManager bungeeManager;
    private final BukkitScheduler scheduler;
    private FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
    private FloodgateService floodgateService;
    private GeyserService geyserService;

    private PremiumPlaceholder premiumPlaceholder;

    public FastLoginBukkit() {
        this.logger = CommonUtil.initializeLoggerService(getLogger());
        this.scheduler = new BukkitScheduler(this, logger, getThreadFactory());
    }

    @Override
    public void onEnable() {
        core = new FastLoginCore<>(this);
        core.load();

        if (getServer().getOnlineMode()) {
            //we need to require offline to prevent a loginSession request for an offline player
            logger.error("Server has to be in offline mode");
            setEnabled(false);
            return;
        }

        if (!initializeFloodgate()) {
            setEnabled(false);
        }

        bungeeManager = new BungeeManager(this);
        bungeeManager.initialize();

        PluginManager pluginManager = getServer().getPluginManager();
        if (bungeeManager.isEnabled()) {
            markInitialized();
        } else {
            if (!core.setupDatabase()) {
                setEnabled(false);
                return;
            }

            if (pluginManager.isPluginEnabled("ProtocolSupport")) {
                pluginManager.registerEvents(new ProtocolSupportListener(this, core.getRateLimiter()), this);
            } else if (pluginManager.isPluginEnabled("ProtocolLib")) {
                ProtocolLibListener.register(this, core.getRateLimiter());

                if (isPluginInstalled("floodgate")) {
                    if (getConfig().getBoolean("floodgatePrefixWorkaround")){
                        ManualNameChange.register(this, floodgateService);
                        logger.info("Floodgate prefix injection workaround has been enabled.");
                        logger.info("If you have problems joining the server, try disabling it in the configuration.");
                    } else {
                        logger.warn("We have detected that you are runnging FastLogin alongside Floodgate and ProtocolLib.");
                        logger.warn("Currently there is an issue with FastLogin that prevents Floodgate name prefixes from showing up "
                                + "when it is together used with ProtocolLib.");
                        logger.warn("If you would like to use Floodgate name prefixes, you can enable an experimental workaround by changing "
                                + "the value 'floodgatePrefixWorkaround' to true in config.yml.");
                        logger.warn("For more information visit https://github.com/games647/FastLogin/issues/493");
                    }
                }

                //if server is using paper - we need to set the skin at pre login anyway, so no need for this listener
                if (!PaperLib.isPaper() && getConfig().getBoolean("forwardSkin")) {
                    pluginManager.registerEvents(new SkinApplyListener(this), this);