/* * Copyright (C) 2021 - 2022 Elytrium * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package net.elytrium.limboauth.handler; import at.favre.lib.crypto.bcrypt.BCrypt; import com.j256.ormlite.dao.Dao; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.scheduler.ScheduledTask; import dev.samstevens.totp.code.CodeVerifier; import dev.samstevens.totp.code.DefaultCodeGenerator; import dev.samstevens.totp.code.DefaultCodeVerifier; import dev.samstevens.totp.time.SystemTimeProvider; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.text.MessageFormat; import java.util.List; import java.util.Locale; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import net.elytrium.limboapi.api.Limbo; import net.elytrium.limboapi.api.LimboSessionHandler; import net.elytrium.limboapi.api.player.LimboPlayer; import net.elytrium.limboauth.LimboAuth; import net.elytrium.limboauth.Settings; import net.elytrium.limboauth.event.PostAuthorizationEvent; import net.elytrium.limboauth.event.PostRegisterEvent; import net.elytrium.limboauth.event.TaskEvent; import net.elytrium.limboauth.migration.MigrationHash; import net.elytrium.limboauth.model.RegisteredPlayer; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.title.Title; public class AuthSessionHandler implements LimboSessionHandler { private static final CodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider()); private final Dao playerDao; private final Player proxyPlayer; private final RegisteredPlayer playerInfo; private final LimboAuth plugin; private final long joinTime = System.currentTimeMillis(); private final BossBar bossBar = BossBar.bossBar( Component.empty(), 1, BossBar.Color.valueOf(Settings.IMP.MAIN.BOSSBAR_COLOR.toUpperCase(Locale.ROOT)), BossBar.Overlay.valueOf(Settings.IMP.MAIN.BOSSBAR_OVERLAY.toUpperCase(Locale.ROOT)) ); private ScheduledTask authMainTask; private LimboPlayer player; private String ip; private int attempts = Settings.IMP.MAIN.LOGIN_ATTEMPTS; private boolean totp = false; public AuthSessionHandler(Dao playerDao, Player proxyPlayer, LimboAuth plugin, RegisteredPlayer playerInfo) { this.playerDao = playerDao; this.proxyPlayer = proxyPlayer; this.plugin = plugin; this.playerInfo = playerInfo; } @Override public void onSpawn(Limbo server, LimboPlayer player) { this.player = player; this.player.disableFalling(); this.ip = this.proxyPlayer.getRemoteAddress().getAddress().getHostAddress(); if (this.playerInfo == null) { this.checkIp(); } else { this.checkCase(); } boolean bossBarEnabled = Settings.IMP.MAIN.ENABLE_BOSSBAR; float bossBarMultiplier = 1000F / Settings.IMP.MAIN.AUTH_TIME; if (bossBarEnabled) { this.proxyPlayer.showBossBar(this.bossBar); } this.authMainTask = this.plugin.getServer().getScheduler().buildTask(this.plugin, () -> { if (System.currentTimeMillis() - this.joinTime > Settings.IMP.MAIN.AUTH_TIME) { this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.TIMES_UP)); return; } if (bossBarEnabled) { long timeSinceJoin = Settings.IMP.MAIN.AUTH_TIME - (System.currentTimeMillis() - AuthSessionHandler.this.joinTime); this.bossBar.name(this.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.BOSSBAR, (int) (timeSinceJoin / 1000F)))); // It is possible, that the progress value can overcome 1, e.g. 1.0000001. this.bossBar.progress(Math.min((timeSinceJoin * bossBarMultiplier) / 1000F, 1F)); } }).repeat(1, TimeUnit.SECONDS).schedule(); this.sendMessage(true); } @Override public void onChat(String message) { String[] args = message.split(" "); if (args.length != 0 && this.checkArgsLength(args.length)) { Command command = Command.parse(args[0]); switch (command) { case REGISTER: { if (!this.totp && this.playerInfo == null) { if (this.checkPasswordsRepeat(args) && this.checkPasswordLength(args[1]) && this.checkPasswordStrength(args[1])) { this.register(args[1]); this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL)); if (!Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_TITLE.isEmpty() && !Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_SUBTITLE.isEmpty()) { this.proxyPlayer.showTitle( Title.title( this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_TITLE), this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUCCESSFUL_SUBTITLE), Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes() ) ); } this.finishRegister(); } } else { this.sendMessage(false); } break; } case LOGIN: { if (!this.totp && this.playerInfo != null) { if (this.checkPassword(args[1])) { this.loginOrTotp(); } else if (--this.attempts != 0) { this.proxyPlayer.sendMessage(this.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD, this.attempts))); } else { this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_WRONG_PASSWORD_KICK)); } } else { this.sendMessage(false); } break; } case TOTP: { if (this.totp) { if (verifier.isValidCode(this.playerInfo.getTotpToken(), args[1])) { this.finishLogin(); } else { this.sendMessage(false); } } else { this.sendMessage(false); } break; } case INVALID: default: { this.sendMessage(false); } } } else { this.sendMessage(false); } } @Override public void onDisconnect() { if (this.authMainTask != null) { this.authMainTask.cancel(); } this.proxyPlayer.hideBossBar(this.bossBar); } public static RegisteredPlayer fetchInfo(Dao playerDao, String nickname) { List playerList = null; try { playerList = playerDao.queryForEq("LOWERCASENICKNAME", nickname.toLowerCase(Locale.ROOT)); } catch (SQLException e) { e.printStackTrace(); } return (playerList != null ? playerList.size() : 0) == 0 ? null : playerList.get(0); } public static RegisteredPlayer fetchInfo(Dao playerDao, UUID uuid) { List playerList = null; try { playerList = playerDao.queryForEq("PREMIUMUUID", uuid.toString()); } catch (SQLException e) { e.printStackTrace(); } return (playerList != null ? playerList.size() : 0) == 0 ? null : playerList.get(0); } public static CodeVerifier getVerifier() { return verifier; } private boolean checkPassword(String password) { return checkPassword(password, this.playerInfo, this.playerDao); } public static boolean checkPassword(String password, RegisteredPlayer player, Dao playerDao) { boolean isCorrect = BCrypt.verifyer().verify( password.getBytes(StandardCharsets.UTF_8), player.getHash().replace("BCRYPT$", "$2a$").getBytes(StandardCharsets.UTF_8) ).verified; if (!isCorrect && !Settings.IMP.MAIN.MIGRATION_HASH.isEmpty()) { isCorrect = MigrationHash.valueOf(Settings.IMP.MAIN.MIGRATION_HASH).checkPassword(player.getHash(), password); if (isCorrect) { player.setHash(genHash(password)); try { playerDao.update(player); } catch (SQLException e) { e.printStackTrace(); return false; } } } return isCorrect; } private void checkIp() { try { List alreadyRegistered = this.playerDao.queryForEq("IP", this.ip); if (alreadyRegistered == null) { return; } AtomicInteger sizeOfValid = new AtomicInteger(alreadyRegistered.size()); if (Settings.IMP.MAIN.IP_LIMIT_VALID_TIME != 0) { long checkDate = System.currentTimeMillis() - Settings.IMP.MAIN.IP_LIMIT_VALID_TIME; alreadyRegistered.stream() .filter(e -> e.getRegDate() != null) .filter(e -> e.getRegDate() < checkDate) .forEach(e -> { try { e.setIP(""); this.playerDao.update(e); sizeOfValid.decrementAndGet(); } catch (SQLException ex) { ex.printStackTrace(); this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.DB_ERROR)); } }); } if (sizeOfValid.get() >= Settings.IMP.MAIN.IP_LIMIT_REGISTRATIONS) { this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.IP_LIMIT)); } } catch (SQLException e) { e.printStackTrace(); this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.DB_ERROR)); } } private void checkCase() { if (!this.proxyPlayer.getUsername().equals(this.playerInfo.getNickname())) { this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.WRONG_NICKNAME_CASE_KICK)); } } private void register(String password) { RegisteredPlayer registeredPlayer = new RegisteredPlayer( this.proxyPlayer.getUsername(), this.proxyPlayer.getUsername().toLowerCase(Locale.ROOT), genHash(password), this.ip, "", System.currentTimeMillis(), this.proxyPlayer.getUniqueId().toString(), "" ); try { this.playerDao.create(registeredPlayer); } catch (SQLException e) { e.printStackTrace(); this.proxyPlayer.disconnect(this.deserialize(Settings.IMP.MAIN.STRINGS.DB_ERROR)); } } private void loginOrTotp() { if (this.playerInfo.getTotpToken().isEmpty()) { this.finishLogin(); } else { this.totp = true; this.sendMessage(true); } } private void finishLogin() { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL)); if (!Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_TITLE.isEmpty() && !Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_SUBTITLE.isEmpty()) { this.proxyPlayer.showTitle( Title.title( this.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_TITLE), this.deserialize(Settings.IMP.MAIN.STRINGS.LOGIN_SUCCESSFUL_SUBTITLE), Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes() ) ); } this.plugin.getServer().getEventManager() .fire(new PostAuthorizationEvent(this.player, this.playerInfo, this::finishAuth)) .thenAcceptAsync(this::finishAuth); } private void finishRegister() { this.plugin.getServer().getEventManager() .fire(new PostRegisterEvent(this.player, this.playerInfo, this::finishAuth)) .thenAcceptAsync(this::finishAuth); } private void finishAuth(TaskEvent event) { if (Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.CLEAR_AFTER_LOGIN) { this.proxyPlayer.clearTitle(); } if (event.getResult() == TaskEvent.Result.CANCEL) { this.proxyPlayer.disconnect(event.getReason()); return; } else if (event.getResult() == TaskEvent.Result.WAIT) { return; } this.plugin.cacheAuthUser(this.proxyPlayer); this.player.disconnect(); } private void sendMessage(boolean sendTitle) { if (this.totp) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.TOTP)); if (sendTitle && !Settings.IMP.MAIN.STRINGS.TOTP_TITLE.isEmpty() && !Settings.IMP.MAIN.STRINGS.TOTP_SUBTITLE.isEmpty()) { this.proxyPlayer.showTitle( Title.title( this.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_TITLE), this.deserialize(Settings.IMP.MAIN.STRINGS.TOTP_SUBTITLE), Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()) ); } } else if (this.playerInfo == null) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER)); if (sendTitle && !Settings.IMP.MAIN.STRINGS.REGISTER_TITLE.isEmpty() && !Settings.IMP.MAIN.STRINGS.REGISTER_SUBTITLE.isEmpty()) { this.proxyPlayer.showTitle( Title.title( this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_TITLE), this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_SUBTITLE), Settings.IMP.MAIN.CRACKED_TITLE_SETTINGS.toTimes()) ); } } else { this.proxyPlayer.sendMessage(this.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN, this.attempts))); if (sendTitle && !Settings.IMP.MAIN.STRINGS.LOGIN_TITLE.isEmpty() && !Settings.IMP.MAIN.STRINGS.LOGIN_SUBTITLE.isEmpty()) { this.proxyPlayer.showTitle( Title.title( this.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_TITLE, this.attempts)), this.deserialize(MessageFormat.format(Settings.IMP.MAIN.STRINGS.LOGIN_SUBTITLE, this.attempts)), Settings.IMP.MAIN.PREMIUM_TITLE_SETTINGS.toTimes() ) ); } } } private boolean checkPasswordLength(String password) { int length = password.length(); if (length > Settings.IMP.MAIN.MAX_PASSWORD_LENGTH) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_TOO_LONG)); return false; } else if (length < Settings.IMP.MAIN.MIN_PASSWORD_LENGTH) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_TOO_SHORT)); return false; } return true; } private boolean checkPasswordStrength(String password) { if (Settings.IMP.MAIN.CHECK_PASSWORD_STRENGTH && this.plugin.getUnsafePasswords().contains(password)) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_PASSWORD_UNSAFE)); return false; } return true; } private boolean checkPasswordsRepeat(String[] args) { if (Settings.IMP.MAIN.REGISTER_NEED_REPEAT_PASSWORD && !args[1].equals(args[2])) { this.proxyPlayer.sendMessage(this.deserialize(Settings.IMP.MAIN.STRINGS.REGISTER_DIFFERENT_PASSWORDS)); return false; } return true; } private boolean checkArgsLength(int argsLength) { if (this.playerInfo == null && Settings.IMP.MAIN.REGISTER_NEED_REPEAT_PASSWORD) { return argsLength == 3; } else { return argsLength == 2; } } private Component deserialize(String text) { return LegacyComponentSerializer.legacyAmpersand().deserialize(text); } public static String genHash(String password) { return BCrypt.withDefaults().hashToString(Settings.IMP.MAIN.BCRYPT_COST, password.toCharArray()); } private enum Command { INVALID, REGISTER, LOGIN, TOTP; static Command parse(String command) { if (Settings.IMP.MAIN.REGISTER_COMMAND.contains(command)) { return Command.REGISTER; } if (Settings.IMP.MAIN.LOGIN_COMMAND.contains(command)) { return Command.LOGIN; } if (Settings.IMP.MAIN.TOTP_COMMAND.contains(command)) { return Command.TOTP; } return Command.INVALID; } } }