Introduced modular components for authentication, including the storage backend

Servers in dev mode now have a no-auth equivalent that allows any user/pass combo without registration
Added a ban command
Added a mute command
Hooked up the mute functionality of the report screen (for pmods+)
Cleaned up all the now-unused classes for player SQL stuff
Player SQL stuff now uses entirely prepared statements
No longer storing PC name, MAC address, serial number as these are inauthentic components of the protocol Packet to be corrected in the future to allow closer compatibility with authentic clients
Used less threading for the SQL queries/updates as these were causing issues both with the old system and the new
Updated ::resetpassword and ::setpasswordother commands to use the new server authentication pipeline (to ensure things are always correctly set)
Refactored the login read event, now handles more exceptions and edge cases
This commit is contained in:
Ceikry
2022-05-16 11:51:42 +00:00
committed by Ryan
parent dd1eed7e39
commit 9ab9885eef
41 changed files with 1264 additions and 1424 deletions
@@ -0,0 +1,22 @@
package rs09.auth
import core.game.node.entity.player.Player
import rs09.storage.AccountStorageProvider
abstract class AuthProvider<T: AccountStorageProvider> {
lateinit var storageProvider: T
abstract fun configureFor(provider: T)
fun canCreateAccountWith(info: UserAccountInfo) : Boolean {
return !storageProvider.checkUsernameTaken(info.username)
}
abstract fun createAccountWith(info: UserAccountInfo) : Boolean
abstract fun checkLogin(username: String, password: String) : Pair<AuthResponse,UserAccountInfo?>
abstract fun checkPassword(player: Player, password: String) : Boolean
abstract fun updatePassword(username: String, newPassword: String)
}
@@ -0,0 +1,27 @@
package rs09.auth
enum class AuthResponse {
UnexpectedError,
CouldNotAd,
Success,
InvalidCredentials,
AccountDisabled,
AlreadyOnline,
Updated,
FullWorld,
LoginServerOffline,
LoginLimitExceeded,
BadSessionID,
WeakPassword,
MembersWorld,
CouldNotLogin,
Updating,
TooManyIncorrectLogins,
StandingInMembersArea,
AccountLocked,
ClosedBeta,
InvalidLoginServer,
MovingWorld,
ErrorLoadingProfile,
BannedUser
}
@@ -0,0 +1,39 @@
package rs09.auth
import core.game.node.entity.player.Player
import rs09.storage.InMemoryStorageProvider
class DevelopmentAuthenticator : AuthProvider<InMemoryStorageProvider>() {
override fun configureFor(provider: InMemoryStorageProvider) {
storageProvider = provider
}
override fun checkLogin(username: String, password: String): Pair<AuthResponse, UserAccountInfo?> {
val info: UserAccountInfo
if(!storageProvider.checkUsernameTaken(username.toLowerCase())) {
info = UserAccountInfo.createDefault()
info.username = username
createAccountWith(info)
} else {
info = storageProvider.getAccountInfo(username.toLowerCase())
}
return Pair(AuthResponse.Success, storageProvider.getAccountInfo(username))
}
override fun createAccountWith(info: UserAccountInfo): Boolean {
info.username = info.username.toLowerCase()
info.rights = 2
storageProvider.store(info)
return true
}
override fun checkPassword(player: Player, password: String): Boolean {
return password == player.details.password
}
override fun updatePassword(username: String, newPassword: String) {
val info = storageProvider.getAccountInfo(username)
info.password = newPassword
storageProvider.update(info)
}
}
@@ -0,0 +1,61 @@
package rs09.auth
import core.game.node.entity.player.Player
import core.game.system.SystemManager
import rs09.ServerConstants
import rs09.storage.AccountStorageProvider
import rs09.storage.SQLStorageProvider
import java.sql.SQLDataException
class ProductionAuthenticator : AuthProvider<AccountStorageProvider>() {
override fun configureFor(provider: AccountStorageProvider) {
storageProvider = provider
if (provider is SQLStorageProvider) {
provider.configure(ServerConstants.DATABASE_ADDRESS!!, ServerConstants.DATABASE_NAME!!, ServerConstants.DATABASE_USER!!, ServerConstants.DATABASE_PASS!!)
}
}
override fun createAccountWith(info: UserAccountInfo): Boolean {
try {
info.password = SystemManager.getEncryption().hashPassword(info.password)
storageProvider.store(info)
} catch (e: SQLDataException) {
return false
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
override fun checkLogin(username: String, password: String): Pair<AuthResponse, UserAccountInfo?> {
val info: UserAccountInfo
try {
if (!storageProvider.checkUsernameTaken(username.toLowerCase())) {
return Pair(AuthResponse.InvalidCredentials, null)
}
info = storageProvider.getAccountInfo(username.toLowerCase())
val passCorrect = SystemManager.getEncryption().checkPassword(password, info.password)
if(!passCorrect || info.password.isEmpty())
return Pair(AuthResponse.InvalidCredentials, null)
if(info.banEndTime > System.currentTimeMillis())
return Pair(AuthResponse.AccountDisabled, null)
if(info.online)
return Pair(AuthResponse.AlreadyOnline, null)
} catch (e: Exception) {
e.printStackTrace()
return Pair(AuthResponse.CouldNotLogin, null)
}
return Pair(AuthResponse.Success, info)
}
override fun checkPassword(player: Player, password: String): Boolean {
return SystemManager.getEncryption().checkPassword(password, player.details.password)
}
override fun updatePassword(username: String, newPassword: String) {
val info = storageProvider.getAccountInfo(username)
info.password = SystemManager.getEncryption().hashPassword(newPassword)
storageProvider.update(info)
}
}
@@ -0,0 +1,81 @@
package rs09.auth
class UserAccountInfo(
var username: String,
var password: String,
var uid: Int,
var rights: Int,
var credits: Int,
var ip: String,
var lastUsedIp: String,
var muteEndTime: Long,
var banEndTime: Long,
var contacts: String,
var blocked: String,
var clanName: String,
var currentClan: String,
var clanReqs: String,
var timePlayed: Long,
var lastLogin: Long,
var online: Boolean
) {
companion object {
@JvmStatic fun createDefault() : UserAccountInfo {
return UserAccountInfo("", "", 0, 0, 0, "", "", 0L, 0L, "", "", "", "", "1,0,8,9", 0L, 0L, false)
}
}
override fun toString(): String {
return "USER:$username,PASS:$password,UID:$uid,RIGHTS:$rights,CREDITS:$credits,IP:$ip,LASTIP:$lastUsedIp"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as UserAccountInfo
if (username != other.username) return false
if (password != other.password) return false
if (uid != other.uid) return false
if (rights != other.rights) return false
if (credits != other.credits) return false
if (ip != other.ip) return false
if (lastUsedIp != other.lastUsedIp) return false
if (muteEndTime != other.muteEndTime) return false
if (banEndTime != other.banEndTime) return false
if (contacts != other.contacts) return false
if (blocked != other.blocked) return false
if (clanName != other.clanName) return false
if (currentClan != other.currentClan) return false
if (clanReqs != other.clanReqs) return false
if (timePlayed != other.timePlayed) return false
if (lastLogin != other.lastLogin) return false
if (online != other.online) return false
return true
}
override fun hashCode(): Int {
var result = username.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + uid
result = 31 * result + rights
result = 31 * result + credits
result = 31 * result + ip.hashCode()
result = 31 * result + lastUsedIp.hashCode()
result = 31 * result + muteEndTime.hashCode()
result = 31 * result + banEndTime.hashCode()
result = 31 * result + contacts.hashCode()
result = 31 * result + blocked.hashCode()
result = 31 * result + clanName.hashCode()
result = 31 * result + currentClan.hashCode()
result = 31 * result + clanReqs.hashCode()
result = 31 * result + timePlayed.hashCode()
result = 31 * result + lastLogin.hashCode()
result = 31 * result + online.hashCode()
return result
}
}
@@ -60,7 +60,7 @@ class EasterBunnyDialogueFile(val NEED_BASKET : Boolean) : DialogueFile() {
player!!.dialogueInterpreter.sendDialogue("You need 5 eggs to afford that.")
} else {
player!!.incrementAttribute(EGG_ATTRIBUTE, -5)
player!!.details.credits += 1
player!!.details.accountInfo.credits += 1
player!!.dialogueInterpreter.sendDialogue(
"You turn in 5 eggs in exchange for a credit.",
"You now have ${player!!.getAttribute(EGG_ATTRIBUTE, 0)} eggs."
@@ -13,6 +13,7 @@ import core.game.system.task.Pulse
import core.net.amsc.MSPacketRepository
import core.net.amsc.ManagementServerState
import core.net.amsc.WorldCommunicator
import rs09.auth.AuthResponse
import rs09.game.system.SystemLogger
import rs09.game.world.GameWorld
import rs09.game.world.GameWorld.loginListeners
@@ -70,7 +71,7 @@ class LoginParser(
} catch (t: Throwable) {
t.printStackTrace()
try {
flag(Response.ERROR_LOADING_PROFILE)
flag(AuthResponse.ErrorLoadingProfile)
Repository.LOGGED_IN_PLAYERS.remove(details.username)
} catch (e: Throwable) {
e.printStackTrace()
@@ -84,10 +85,6 @@ class LoginParser(
*/
private fun handleLogin() {
val p = worldInstance
if (!details.parse()) {
flag(Response.INVALID_LOGIN_SERVER)
return
}
val player = p ?: Player(details)
player.setAttribute("login_type", type)
if (p != null) { // Reconnecting
@@ -122,7 +119,7 @@ class LoginParser(
Repository.lobbyPlayers.remove(player)
Repository.playerNames.remove(player.name)
MSPacketRepository.sendPlayerRemoval(player.name)
flag(Response.ERROR_LOADING_PROFILE)
flag(AuthResponse.ErrorLoadingProfile)
}
//Repository.getPlayerNames().put(player.getName(), player);
GameWorld.Pulser.submit(object : Pulse(1) {
@@ -140,7 +137,7 @@ class LoginParser(
Repository.addPlayer(player)
}
player.details.session.setObject(player)
flag(Response.SUCCESSFUL)
flag(AuthResponse.Success)
player.init()
player.monitor.log(player.details.ipAddress, PlayerMonitor.ADDRESS_LOG)
player.monitor.log(player.details.serial, PlayerMonitor.ADDRESS_LOG)
@@ -156,7 +153,7 @@ class LoginParser(
Repository.lobbyPlayers.remove(player)
Repository.playerNames.remove(player.name)
MSPacketRepository.sendPlayerRemoval(player.name)
flag(Response.ERROR_LOADING_PROFILE)
flag(AuthResponse.ErrorLoadingProfile)
}
return true
}
@@ -185,7 +182,7 @@ class LoginParser(
Repository.disconnectionQueue.remove(details.username)
player.initReconnect()
player.isActive = true
flag(Response.SUCCESSFUL)
flag(AuthResponse.Success)
player.updateSceneGraph(true)
player.configManager.init()
LoginConfiguration.configureGameWorld(player)
@@ -208,22 +205,22 @@ class LoginParser(
//This is supposed to prevent the double-logging issue. Will it work? Who knows.
if (Repository.LOGGED_IN_PLAYERS.contains(details.username)) {
SystemLogger.logWarn("LOGGED_IN_PLAYERS contains ${details.username}")
return flag(Response.ALREADY_ONLINE)
return flag(AuthResponse.AlreadyOnline)
}
if (WorldCommunicator.getState() == ManagementServerState.CONNECTING) {
return flag(Response.LOGIN_SERVER_OFFLINE)
return flag(AuthResponse.LoginServerOffline)
}
if (!details.session.isActive) {
return false
}
if (SystemManager.isUpdating()) {
return flag(Response.UPDATING)
return flag(AuthResponse.Updating)
}
if (Repository.getPlayerByName(details.username).also { gamePlayer = it } != null && gamePlayer!!.session.isActive) {
return flag(Response.ALREADY_ONLINE)
return flag(AuthResponse.AlreadyOnline)
}
return if (details.isBanned) {
flag(Response.ACCOUNT_DISABLED)
flag(AuthResponse.AccountDisabled)
} else true
}
@@ -232,9 +229,9 @@ class LoginParser(
* @param response the [Response].
* @return `True` if successfully logged in.
*/
fun flag(response: Response): Boolean {
fun flag(response: AuthResponse): Boolean {
details.session.write(response, true)
return response == Response.SUCCESSFUL
return response == AuthResponse.Success
}
companion object {
@@ -0,0 +1,27 @@
package rs09.game.system
import rs09.auth.AuthProvider
import rs09.auth.DevelopmentAuthenticator
import rs09.auth.ProductionAuthenticator
import rs09.storage.AccountStorageProvider
import rs09.storage.InMemoryStorageProvider
import rs09.storage.SQLStorageProvider
object Auth {
lateinit var authenticator: AuthProvider<*>
lateinit var storageProvider: AccountStorageProvider
fun configureFor(devMode: Boolean) {
if (devMode) {
authenticator = DevelopmentAuthenticator()
storageProvider = InMemoryStorageProvider()
(authenticator as DevelopmentAuthenticator).configureFor(storageProvider as InMemoryStorageProvider)
SystemLogger.logInfo("[AUTH] Using Development Authenticator with In-Memory Storage")
} else {
authenticator = ProductionAuthenticator()
storageProvider = SQLStorageProvider()
(authenticator as ProductionAuthenticator).configureFor(storageProvider as SQLStorageProvider)
SystemLogger.logInfo("[AUTH] Using Production Authenticator with SQL Storage")
}
}
}
@@ -2,7 +2,6 @@ package rs09.game.system.command.sets
import core.game.node.entity.player.Player
import core.game.node.entity.player.info.Rights
import rs09.game.system.command.Command
import core.game.system.task.Pulse
import rs09.game.world.GameWorld
import core.game.world.map.Location
@@ -25,7 +24,7 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){
* Kick a player
* =============================================================================================================
*/
define("kick", Privilege.ADMIN){ player, args ->
define("kick", Privilege.MODERATOR){ player, args ->
val playerToKick: Player? = Repository.getPlayerByName(args[1])
if (playerToKick != null) {
playerToKick.clear(true)
@@ -38,6 +37,100 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){
* =============================================================================================================
*/
/**
* Ban a player
* =============================================================================================================
*/
define("ban", Privilege.ADMIN){ player, args ->
val name = args[1]
if(!GameWorld.accountStorage.checkUsernameTaken(name)) {
reject(player, "Invalid username: $name")
}
val playerToKick: Player? = Repository.getPlayerByName(name)
val durationString = args[2]
val durationTokens = durationString.toCharArray()
var intToken = ""
var durationMillis = 0L
var durationUnit: TimeUnit = TimeUnit.NANOSECONDS
for(token in durationTokens){
if(token.toString().toIntOrNull() != null) intToken += token
else {
val durationInt: Int = (intToken.toIntOrNull() ?: -1).also { if(it == -1) reject(player, "Invalid duration: $intToken") }
durationUnit = when(token) {
'd' -> TimeUnit.DAYS
's' -> TimeUnit.SECONDS
'm' -> TimeUnit.MINUTES
'h' -> TimeUnit.HOURS
else -> TimeUnit.SECONDS
}
durationMillis = durationUnit.toMillis(durationInt.toLong())
}
}
playerToKick?.details?.accountInfo?.banEndTime = System.currentTimeMillis() + durationMillis
playerToKick?.clear(true)
GameWorld.Pulser.submit(object : Pulse(2) {
override fun pulse(): Boolean {
val info = GameWorld.accountStorage.getAccountInfo(name)
info.banEndTime = System.currentTimeMillis() + durationMillis
GameWorld.accountStorage.update(info)
return true
}
})
notify(player, "Banned user $name for $intToken ${durationUnit.name.toLowerCase()}.")
}
/**
* =============================================================================================================
*/
/**
* Mute a player
* =============================================================================================================
*/
define("mute", Privilege.MODERATOR){ player, args ->
val name = args[1]
if(!GameWorld.accountStorage.checkUsernameTaken(name)) {
reject(player, "Invalid username: $name")
}
val playerToMute: Player? = Repository.getPlayerByName(name)
val durationString = args[2]
val durationTokens = durationString.toCharArray()
var intToken = ""
var durationMillis = 0L
var durationUnit: TimeUnit = TimeUnit.NANOSECONDS
for(token in durationTokens){
if(token.toString().toIntOrNull() != null) intToken += token
else {
val durationInt: Int = (intToken.toIntOrNull() ?: -1).also { if(it == -1) reject(player, "Invalid duration: $intToken") }
durationUnit = when(token) {
'd' -> TimeUnit.DAYS
's' -> TimeUnit.SECONDS
'm' -> TimeUnit.MINUTES
'h' -> TimeUnit.HOURS
else -> TimeUnit.SECONDS
}
durationMillis = durationUnit.toMillis(durationInt.toLong())
}
}
playerToMute?.details?.accountInfo?.muteEndTime = System.currentTimeMillis() + durationMillis
if(playerToMute == null) { //Player was offline at the time
GameWorld.Pulser.submit(object : Pulse(2) {
override fun pulse(): Boolean {
val info = GameWorld.accountStorage.getAccountInfo(name)
info.muteEndTime = System.currentTimeMillis() + durationMillis
GameWorld.accountStorage.update(info)
return true
}
})
}
notify(player, "Muted user $name for $intToken ${durationUnit.name.toLowerCase()}.")
}
/**
* =============================================================================================================
*/
/**
* Jail a player
@@ -1,17 +1,18 @@
package rs09.game.system.command.sets
import core.cache.Cache
import core.cache.def.Definition
import api.InputType
import api.runTask
import api.sendDialogue
import api.sendInputDialogue
import core.cache.def.impl.ItemDefinition
import core.game.node.entity.player.info.login.Response
import core.game.node.entity.player.info.portal.PlayerSQLManager
import core.game.node.item.Item
import core.game.system.SystemManager
import core.game.system.SystemState
import core.plugin.Initializable
import org.rs09.consts.Items
import rs09.game.system.command.Command
import rs09.game.system.SystemLogger
import rs09.game.system.command.Privilege
import rs09.game.world.GameWorld
import rs09.game.world.repository.Repository
import kotlin.system.exitProcess
@@ -40,31 +41,29 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) {
* Allows a player to reset their password
*/
define("resetpassword", Privilege.STANDARD) { player, args ->
if (args.size != 3) {
reject(player, "Usage: ::resetpassword current new", "WARNING: THIS IS PERMANENT.", "WARNING: PASSWORD CAN NOT CONTAIN SPACES.")
sendInputDialogue(player, InputType.STRING_SHORT, "Enter Current Password:"){value ->
val pass = value.toString()
SystemLogger.logInfo(pass)
runTask(player) {
if (GameWorld.authenticator.checkPassword(player, pass)) {
sendInputDialogue(player, InputType.STRING_SHORT, "Enter New Password:") { value2 ->
val newPass = value2.toString()
if (pass == newPass) {
sendDialogue(player, "Failed: Passwords Match")
} else if (newPass.length !in 5..20) {
sendDialogue(player, "Failed: Password Too Long Or Too Short")
} else if (newPass == player.details.accountInfo.username) {
sendDialogue(player, "Failed: Password Is Username")
} else {
GameWorld.authenticator.updatePassword(player.details.accountInfo.username, newPass)
sendDialogue(player, "Success: Password Updated!")
}
}
} else {
sendDialogue(player, "Fail: Wrong Password.")
}
}
}
val oldPass = args[1]
var newPass = args[2]
if (PlayerSQLManager.getCredentialResponse(player.details.username, oldPass) != Response.SUCCESSFUL) {
reject(player, "INVALID PASSWORD!")
}
if (newPass.length < 5 || newPass.length > 20) {
reject(player, "NEW PASSWORD MUST BE BETWEEN 5 AND 20 CHARACTERS")
}
if (newPass == player.username) {
reject(player, "PASSWORD CAN NOT BE SAME AS USERNAME.")
}
if (newPass == oldPass) {
reject(player, "PASSWORDS CAN NOT BE THE SAME")
}
newPass = SystemManager.getEncryption().hashPassword(newPass)
PlayerSQLManager.updatePassword(player.username.toLowerCase().replace(" ", "_"), newPass)
notify(player, "Password updated successfully.")
}
/**
@@ -75,9 +74,9 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) {
reject(player, "Usage: ::resetpasswordother user new", "WARNING: THIS IS PERMANENT.", "WARNING: PASSWORD CAN NOT CONTAIN SPACES.")
}
val otherUser = args[1]
var newPass = args[2]
val newPass = args[2]
if (PlayerSQLManager.hasSqlAccount(otherUser, "username")) {
if (GameWorld.accountStorage.checkUsernameTaken(otherUser)) {
if (newPass.length < 5 || newPass.length > 20) {
reject(player, "NEW PASSWORD MUST BE BETWEEN 5 AND 20 CHARACTERS")
@@ -87,8 +86,7 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) {
reject(player, "PASSWORD CAN NOT BE SAME AS USERNAME.")
}
newPass = SystemManager.getEncryption().hashPassword(newPass)
PlayerSQLManager.updatePassword(otherUser.toLowerCase().replace(" ","_"),newPass)
GameWorld.authenticator.updatePassword(otherUser, newPass)
notify(player, "Password updated successfully.")
} else {
@@ -12,13 +12,15 @@ import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.plugin.CorePluginTypes.StartupPlugin
import core.tools.RandomFunction
import core.tools.mysql.DatabaseManager
import rs09.ServerConstants
import rs09.auth.AuthProvider
import rs09.game.system.Auth
import rs09.game.system.SystemLogger
import rs09.game.system.SystemLogger.logInfo
import rs09.game.system.config.ConfigParser
import rs09.game.world.repository.Repository
import rs09.plugin.ClassScanner
import rs09.storage.AccountStorageProvider
import rs09.worker.MajorUpdateWorker
import java.text.SimpleDateFormat
import java.util.*
@@ -83,14 +85,17 @@ object GameWorld {
*/
@JvmStatic
var settings: GameSettings? = null
@JvmStatic
val authenticator: AuthProvider<*>
get() = Auth.authenticator
@JvmStatic
val accountStorage: AccountStorageProvider
get() = Auth.storageProvider
/**
* The current amount of (600ms) cycles elapsed.
*/
@JvmStatic
var ticks = 0
@JvmStatic
var databaseManager: DatabaseManager? = null
private set
@JvmStatic
var Pulser = PulseRunner()
@@ -158,9 +163,12 @@ object GameWorld {
fun prompt(run: Boolean, directory: String?){
logInfo("Prompting ${settings?.name} Game World...")
Cache.init(ServerConstants.CACHE_PATH)
databaseManager = DatabaseManager(ServerConstants.DATABASE)
databaseManager!!.connect()
configParser.prePlugin()
//go overboard with checks to make sure dev mode authenticator never triggers on live
Auth.configureFor(
settings!!.isDevMode
&& ServerConstants.MS_SECRET_KEY == "2009scape_development"
)
ConfigParser().prePlugin()
ClassScanner.scanClasspath()
ClassScanner.loadPureInterfaces()
worldPersists.forEach { it.parse() }
@@ -195,12 +195,6 @@ class DisconnectionQueue {
fun save(player: Player, sql: Boolean): Boolean {
try {
PlayerParser.save(player)
if (sql) {
player.details.sqlManager.update(player)
player.details.save()
/*SQLEntryHandler.write(HighscoreSQLHandler(player))
SQLEntryHandler.write(PlayerLogSQLHandler(player.monitor, player.name))*/
}
return true
} catch (t: Throwable) {
t.printStackTrace()
@@ -1,190 +1,39 @@
package rs09.net.event
import core.cache.Cache
import core.cache.crypto.ISAACCipher
import core.cache.crypto.ISAACPair
import core.cache.misc.buffer.ByteBufferUtils
import core.game.node.entity.player.info.ClientInfo
import core.game.node.entity.player.info.PlayerDetails
import core.game.node.entity.player.info.UIDInfo
import core.game.node.entity.player.info.login.LoginType
import core.game.node.entity.player.info.login.Response
import core.game.node.entity.player.info.portal.PlayerSQLManager
import core.game.system.task.TaskExecutor
import core.net.Constants
import core.net.IoReadEvent
import core.net.IoSession
import core.net.amsc.WorldCommunicator
import core.tools.StringUtils
import rs09.ServerConstants
import rs09.game.node.entity.player.info.login.LoginParser
import rs09.game.system.SystemLogger
import rs09.game.world.repository.Repository
import java.lang.Runnable
import java.math.BigInteger
import rs09.auth.AuthResponse
import rs09.game.world.GameWorld
import rs09.net.packet.`in`.Login
import java.nio.ByteBuffer
import java.util.*
/**
* Handles login reading events.
* @author Emperor
* @author Ceikry
*/
class LoginReadEvent
/**
* Constructs a new `LoginReadEvent`.
* @param session The session.
* @param buffer The buffer with data to read from.
*/
(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(session, buffer) {
class LoginReadEvent(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(session, buffer) {
override fun read(session: IoSession, buffer: ByteBuffer) {
SystemLogger.logInfo("login read")
val opcode: Int = buffer.get().toInt()
if (buffer.short.toInt() != buffer.remaining()) {
session.write(Response.BAD_SESSION_ID)
val (response, info) = Login.decodeFromBuffer(buffer)
if(response != AuthResponse.Success || info == null) {
session.write(response)
return
}
val build = buffer.int
if (build != Constants.REVISION) { // || buffer.getInt() != Constants.CLIENT_BUILD) {
session.write(Response.UPDATED)
val (authResponse, accountInfo) = GameWorld.authenticator.checkLogin(info.username, info.password)
if(authResponse != AuthResponse.Success || accountInfo == null) {
session.write(authResponse)
return
}
when (opcode) {
12 -> println("User details event detected")
16, 18 -> decodeWorld(opcode, session, buffer)
else -> {
SystemLogger.logErr("[Login] Unhandled login type [opcode=$opcode]!")
session.disconnect()
}
}
}
companion object {
val details = PlayerDetails(info.username)
details.accountInfo = accountInfo
session.clientInfo = ClientInfo(info.displayMode, info.windowMode, info.screenWidth, info.screenHeight)
session.isaacPair = info.isaacPair
/**
* Decodes a world login request.
* @param session The session.
* @param buffer The buffer to read from.
*/
private fun decodeWorld(opcode: Int, session: IoSession, buffer: ByteBuffer) {
var buffer = buffer
val d = buffer.get() // Memory?
val e = buffer.get() // no advertisement = 1
val f = buffer.get() // 1
val windowMode = buffer.get().toInt() // Screen size mode
val screenWidth = buffer.short.toInt() // Screen size Width
val screenHeight = buffer.short.toInt() // Screen size Height
val displayMode = buffer.get().toInt() // Display mode
val data = ByteArray(24) // random.dat data.
buffer[data]
ByteBufferUtils.getString(buffer)
buffer.int // Affiliate id
buffer.int // Hash containing a bunch of settings
val curpackets = buffer.short //Current interface packet counter.
for (i in Cache.getIndexes().indices) {
val crc = if (Cache.getIndexes()[i] == null) 0 else Cache.getIndexes()[i].information.informationContainer.crc
if (crc != buffer.int && crc != 0) {
/*session.write(Response.UPDATED);
return;*/
}
}
buffer = getRSABlock(buffer)
buffer.rewind()
if(buffer.get().toInt() != 10){
session.write(Response.COULD_NOT_LOGIN)
return
}
val isaacSeed = getISAACSeed(buffer)
val inCipher = ISAACCipher(isaacSeed)
for(i in 0..curpackets){
inCipher.nextValue
}
for (i in 0..3) {
isaacSeed[i] += 50
}
val outCipher = ISAACCipher(isaacSeed)
session.isaacPair = ISAACPair(inCipher, outCipher)
session.clientInfo = ClientInfo(displayMode, windowMode, screenWidth, screenHeight)
val b = buffer
SystemLogger.logInfo("spawning thread to handle login")
TaskExecutor.executeSQL {
SystemLogger.logInfo("login thread start")
Thread.currentThread().name = "Login Password Response"
SystemLogger.logInfo("login thread named")
try {
val username = StringUtils.longToString(b.long)
SystemLogger.logInfo("got username")
val password = ByteBufferUtils.getString(b)
SystemLogger.logInfo("got password")
val response = PlayerSQLManager.getCredentialResponse(username, password)
SystemLogger.logInfo("got sql response")
if (response != Response.SUCCESSFUL) {
SystemLogger.logInfo("not success :(")
session.write(response, true)
return@executeSQL
}
SystemLogger.logInfo("great success, attempting login")
login(PlayerDetails(username, password), session, b, opcode)
SystemLogger.logInfo("done")
} catch (e: Exception) {
SystemLogger.logInfo("big whoops")
e.printStackTrace()
session.write(Response.COULD_NOT_LOGIN)
}
SystemLogger.logInfo("end login thread")
}
}
/**
* Handles the login procedure after we check an acc is registered & certified.
* @param details the player's details.
* @param session the session.
* @param buffer the byte buffer.
* @param opcode the opcode.
*/
@JvmStatic
private fun login(details: PlayerDetails, session: IoSession, buffer: ByteBuffer, opcode: Int) {
SystemLogger.logInfo("login")
if(!Repository.LOGGED_IN_PLAYERS.contains(details.username))
Repository.LOGGED_IN_PLAYERS.add(details.username)
val parser = LoginParser(details, LoginType.fromType(opcode))
details.session = session
details.info.translate(UIDInfo(details.ipAddress, ByteBufferUtils.getString(buffer), ByteBufferUtils.getString(buffer), ByteBufferUtils.getString(buffer)))
if (WorldCommunicator.isEnabled()) {
WorldCommunicator.register(parser)
} else {
TaskExecutor.executeSQL {parser.run()}
}
}
/**
* Gets the ISAAC seed from the buffer.
* @param buffer The buffer to read from.
* @return The ISAAC seed.
*/
@JvmStatic
fun getISAACSeed(buffer: ByteBuffer): IntArray {
val seed = IntArray(4)
for (i in 0..3) {
seed[i] = buffer.int
}
return seed
}
/**
* Gets the RSA block buffer.
* @param buffer The buffer to get the RSA block from.
* @return The RSA block buffer.
*/
@JvmStatic
fun getRSABlock(buffer: ByteBuffer): ByteBuffer {
SystemLogger.logInfo("getRSABlock")
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
SystemLogger.logInfo(buffer.array().sliceArray(0..1500).toHex())
val numBytes = 256 + buffer.get()
val encryptedByteArray = ByteArray(numBytes)
buffer.get(encryptedByteArray)
val encryptedBytes = BigInteger(encryptedByteArray)
return ByteBuffer.wrap(encryptedBytes.modPow(ServerConstants.EXPONENT, ServerConstants.MODULUS).toByteArray())
}
Login.proceedWith(session, details, info.opcode)
}
}
@@ -0,0 +1,114 @@
package rs09.net.packet.`in`
import core.cache.crypto.ISAACCipher
import core.cache.crypto.ISAACPair
import core.cache.misc.buffer.ByteBufferUtils
import core.game.node.entity.player.info.PlayerDetails
import core.game.node.entity.player.info.UIDInfo
import core.game.node.entity.player.info.login.LoginType
import core.net.Constants
import core.net.IoSession
import core.net.amsc.WorldCommunicator
import core.tools.StringUtils
import rs09.ServerConstants
import rs09.auth.AuthResponse
import rs09.game.node.entity.player.info.login.LoginParser
import rs09.game.system.SystemLogger
import rs09.game.world.repository.Repository
import java.math.BigInteger
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
object Login {
private const val ENCRYPTION_VERIFICATION_BYTE: Int = 10
private const val NORMAL_LOGIN_OP = 16
private const val RECONNECT_LOGIN_OP = 18
const val CACHE_INDEX_COUNT = 29
fun decodeFromBuffer(buffer: ByteBuffer) : Pair<AuthResponse, LoginInfo?> {
try {
val info = LoginInfo.createDefault()
info.opcode = buffer.get().toInt()
if (buffer.short.toInt() != buffer.remaining()) {
return Pair(AuthResponse.BadSessionID, null)
}
val revision = buffer.int
if (revision != Constants.REVISION) {
return Pair(AuthResponse.Updated, null)
}
if (info.opcode != NORMAL_LOGIN_OP && info.opcode != RECONNECT_LOGIN_OP) {
SystemLogger.logInfo("Invalid Login Opcode: ${info.opcode}")
return Pair(AuthResponse.InvalidLoginServer, null)
}
noop(buffer)
info.showAds = buffer.get().toInt() == 1
noop(buffer)
info.windowMode = buffer.get().toInt()
info.screenWidth = buffer.short.toInt()
info.screenHeight = buffer.short.toInt()
info.displayMode = buffer.get().toInt()
noop(buffer, 24) //Skip past a bunch of random (actually random) data the client sends
ByteBufferUtils.getString(buffer) //same as above
info.adAffiliateId = buffer.int
info.settingsHash = buffer.int
info.currentPacketCount = buffer.short.toInt()
//Read client-reported CRC sums
for (i in 0 until CACHE_INDEX_COUNT) info.crcSums[i] = buffer.int
val decryptedBuffer = decryptRSABuffer(buffer, ServerConstants.EXPONENT, ServerConstants.MODULUS)
decryptedBuffer.rewind()
if (decryptedBuffer.get().toInt() != ENCRYPTION_VERIFICATION_BYTE) {
return Pair(AuthResponse.UnexpectedError, info)
}
info.isaacPair = produceISAACPairFrom(decryptedBuffer)
info.username = StringUtils.longToString(decryptedBuffer.long)
info.password = ByteBufferUtils.getString(decryptedBuffer)
return Pair(AuthResponse.Success, info)
} catch (e: Exception) {
SystemLogger.logErr("Exception encountered during login packet parsing! See stack trace below.")
e.printStackTrace()
return Pair(AuthResponse.UnexpectedError, null)
}
}
private fun produceISAACPairFrom(buffer: ByteBuffer): ISAACPair {
val incomingSeed = IntArray(4)
for(i in incomingSeed.indices) {
incomingSeed[i] = buffer.int
}
val inCipher = ISAACCipher(incomingSeed)
for(i in incomingSeed.indices) {
incomingSeed[i] += 50
}
val outCipher = ISAACCipher(incomingSeed)
return ISAACPair(inCipher, outCipher)
}
@JvmStatic fun decryptRSABuffer(buffer: ByteBuffer, exponent: BigInteger, modulus: BigInteger): ByteBuffer {
return try {
val numBytes = buffer.get().toInt() and 0xFF
val encryptedBytes = ByteArray(numBytes)
buffer[encryptedBytes]
val encryptedBigInt = BigInteger(encryptedBytes)
ByteBuffer.wrap(encryptedBigInt.modPow(exponent, modulus).toByteArray())
} catch (e: BufferUnderflowException) {
ByteBuffer.wrap(byteArrayOf(-1))
}
}
private fun noop(buffer: ByteBuffer, amount: Int = 1) {buffer[ByteArray(amount)]}
fun proceedWith(session: IoSession, details: PlayerDetails, opcode: Int) {
if (!Repository.LOGGED_IN_PLAYERS.contains(details.username))
Repository.LOGGED_IN_PLAYERS.add(details.username)
details.session = session
details.info.translate(UIDInfo(details.ipAddress, "DEPRECATED", "DEPRECATED", "DEPRECATED"))
WorldCommunicator.register(LoginParser(details, LoginType.fromType(opcode)))
}
}
@@ -0,0 +1,30 @@
package rs09.net.packet.`in`
import core.cache.crypto.ISAACCipher
import core.cache.crypto.ISAACPair
class LoginInfo(
var showAds: Boolean, //Unused
var windowMode : Int,
var screenWidth: Int,
var screenHeight: Int,
var displayMode: Int,
var adAffiliateId: Int, //Unused
var settingsHash: Int,
var currentPacketCount: Int,
var username: String,
var password: String,
var isaacPair: ISAACPair,
var opcode: Int,
var crcSums: IntArray
) {
companion object {
fun createDefault(): LoginInfo {
return LoginInfo(false, 0, 0, 0, 0, 0, 0, 0, "", "", ISAACPair(ISAACCipher(intArrayOf()), ISAACCipher(intArrayOf())), 0, IntArray(Login.CACHE_INDEX_COUNT))
}
}
override fun toString(): String {
return "ads:$showAds,wm:$windowMode,sw:$screenWidth,sh:$screenHeight,dm:$displayMode,adid:$adAffiliateId,settings:$settingsHash,pkt:$currentPacketCount,un:$username,pw:$password"
}
}
@@ -0,0 +1,11 @@
package rs09.storage
import rs09.auth.UserAccountInfo
interface AccountStorageProvider {
fun checkUsernameTaken(username: String): Boolean
fun getAccountInfo(username: String): UserAccountInfo
fun store(info: UserAccountInfo)
fun update(info: UserAccountInfo)
fun remove(info: UserAccountInfo)
}
@@ -0,0 +1,27 @@
package rs09.storage
import rs09.auth.UserAccountInfo
class InMemoryStorageProvider : AccountStorageProvider {
private val storage = HashMap<String, UserAccountInfo>()
override fun checkUsernameTaken(username: String): Boolean {
return storage[username] != null
}
override fun getAccountInfo(username: String): UserAccountInfo {
return storage[username] ?: UserAccountInfo.createDefault()
}
override fun store(info: UserAccountInfo) {
storage[info.username] = info
}
override fun update(info: UserAccountInfo) {
storage[info.username] = info
}
override fun remove(info: UserAccountInfo) {
storage.remove(info.username)
}
}
@@ -0,0 +1,214 @@
package rs09.storage
import rs09.auth.UserAccountInfo
import java.lang.Long.max
import java.sql.*
class SQLStorageProvider : AccountStorageProvider {
var connectionString = ""
var connectionUsername = ""
var connectionPassword = ""
fun getConnection(): Connection {
Class.forName("com.mysql.cj.jdbc.Driver")
return DriverManager.getConnection(connectionString, connectionUsername, connectionPassword)
}
fun configure(host: String, databaseName: String, username: String, password: String) {
connectionString = "jdbc:mysql://$host/$databaseName?useTimezone=true&serverTimezone=UTC"
connectionUsername = username
connectionPassword = password
}
override fun checkUsernameTaken(username: String): Boolean {
val conn = getConnection()
conn.use {
val compiledUsernameQuery = it.prepareStatement(usernameQuery)
compiledUsernameQuery.setString(1, username.toLowerCase())
val result = compiledUsernameQuery.executeQuery()
val exists = result.next()
result.close()
return exists
}
}
override fun getAccountInfo(username: String): UserAccountInfo {
val conn = getConnection()
conn.use { con ->
val compiledAccountInfoQuery = con.prepareStatement(accountInfoQuery)
compiledAccountInfoQuery.setString(1, username.toLowerCase())
val result = compiledAccountInfoQuery.executeQuery()
if (result.next()) {
val userData = UserAccountInfo.createDefault()
userData.username = username
result.getString(2) ?.let { userData.password = it }
result.getInt(3) .let { userData.uid = it }
result.getInt(4) .let { userData.rights = it }
result.getInt(5) .let { userData.credits = it }
result.getString(6) ?.let { userData.ip = it }
result.getString(7) ?.let { userData.lastUsedIp = it }
result.getLong(8) .let { userData.muteEndTime = max(0L, it) }
result.getLong(9) .let { userData.banEndTime = max(0L, it) }
result.getString(10)?.let { userData.contacts = it }
result.getString(11)?.let { userData.blocked = it }
result.getString(12)?.let { userData.clanName = it }
result.getString(13)?.let { userData.currentClan = it }
result.getString(14)?.let { userData.clanReqs = it }
result.getLong(15) .let { userData.timePlayed = max(0L, it) }
result.getLong(16) .let { userData.lastLogin = max(0L, it) }
result.getBoolean(17).let { userData.online = it }
return userData
} else {
return UserAccountInfo.createDefault()
}
}
}
override fun store(info: UserAccountInfo) {
val conn = getConnection()
conn.use {
val compiledInsertInfoQuery = it.prepareStatement(insertInfoQuery, Statement.RETURN_GENERATED_KEYS)
val emptyInfo = UserAccountInfo.createDefault()
if (info == emptyInfo) {
throw IllegalStateException("Tried to store empty data!")
}
emptyInfo.username = info.username
if (info == emptyInfo) {
throw IllegalStateException("Tried to store empty data!")
}
if (checkUsernameTaken(info.username)) {
throw SQLDataException("Account already exists!")
}
compiledInsertInfoQuery.setString(1, info.username)
compiledInsertInfoQuery.setString(2, info.password)
compiledInsertInfoQuery.setInt(3, info.rights)
compiledInsertInfoQuery.setInt(4, info.credits)
compiledInsertInfoQuery.setString(5, info.ip)
compiledInsertInfoQuery.setString(6, info.ip)
compiledInsertInfoQuery.setLong(7, info.muteEndTime)
compiledInsertInfoQuery.setLong(8, info.banEndTime)
compiledInsertInfoQuery.setString(9, info.contacts)
compiledInsertInfoQuery.setString(10, info.blocked)
compiledInsertInfoQuery.setString(11, info.clanName)
compiledInsertInfoQuery.setString(12, info.currentClan)
compiledInsertInfoQuery.setString(13, info.clanReqs)
compiledInsertInfoQuery.setLong(14, info.timePlayed)
compiledInsertInfoQuery.setLong(15, info.lastLogin)
compiledInsertInfoQuery.setBoolean(16, info.online)
compiledInsertInfoQuery.execute()
val result = compiledInsertInfoQuery.generatedKeys
if (result.next()) {
info.uid = result.getInt(1)
}
}
}
override fun update(info: UserAccountInfo) {
val conn = getConnection()
conn.use {
val compiledUpdateInfoQuery = it.prepareStatement(updateInfoQuery)
val emptyInfo = UserAccountInfo.createDefault()
if (info == emptyInfo) {
throw IllegalStateException("Tried to store empty data!")
}
emptyInfo.username = info.username
if (info == emptyInfo) {
throw IllegalStateException("Tried to store empty data!")
}
emptyInfo.password = info.password
if (info == emptyInfo) {
throw IllegalStateException("Tried to store empty data!")
}
compiledUpdateInfoQuery.setString(1, info.username)
compiledUpdateInfoQuery.setString(2, info.password)
compiledUpdateInfoQuery.setInt(3, info.rights)
compiledUpdateInfoQuery.setInt(4, info.credits)
compiledUpdateInfoQuery.setString(5, info.ip)
compiledUpdateInfoQuery.setLong(6, info.muteEndTime)
compiledUpdateInfoQuery.setLong(7, info.banEndTime)
compiledUpdateInfoQuery.setString(8, info.contacts)
compiledUpdateInfoQuery.setString(9, info.blocked)
compiledUpdateInfoQuery.setString(10, info.clanName)
compiledUpdateInfoQuery.setString(11, info.currentClan)
compiledUpdateInfoQuery.setString(12, info.clanReqs)
compiledUpdateInfoQuery.setLong(13, info.timePlayed)
compiledUpdateInfoQuery.setLong(14, info.lastLogin)
compiledUpdateInfoQuery.setBoolean(15, info.online)
compiledUpdateInfoQuery.setInt(16, info.uid)
compiledUpdateInfoQuery.execute()
}
}
override fun remove(info: UserAccountInfo) {
val conn = getConnection()
conn.use {
val compiledRemoveInfoQuery = it.prepareStatement(removeInfoQuery)
compiledRemoveInfoQuery.setString(1, info.username)
compiledRemoveInfoQuery.execute()
}
}
companion object {
private const val usernameQuery = "SELECT username FROM members WHERE username = ?;"
private const val removeInfoQuery = "DELETE FROM members WHERE username = ?;"
private const val accountInfoQuery = "SELECT " +
"username," +
"password," +
"UID," +
"rights," +
"credits," +
"ip," +
"lastGameIp," +
"muteTime," +
"banTime," +
"contacts," +
"blocked," +
"clanName," +
"currentClan," +
"clanReqs," +
"timePlayed," +
"lastLogin," +
"online" +
" FROM members WHERE username = ?;"
private const val insertInfoQuery = "INSERT INTO members (" +
"username," +
"password," +
"rights," +
"credits," +
"ip," +
"lastGameIp," +
"muteTime," +
"banTime," +
"contacts," +
"blocked," +
"clanName," +
"currentClan," +
"clanReqs," +
"timePlayed," +
"lastLogin," +
"online" +
") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"
private const val updateInfoQuery = "UPDATE members SET " +
"username = ?," +
"password = ?," +
"rights = ?," +
"credits = ?," +
"lastGameIp = ?," +
"muteTime = ?," +
"banTime = ?," +
"contacts = ?," +
"blocked = ?," +
"clanName = ?," +
"currentClan = ?," +
"clanReqs = ?," +
"timePlayed = ?," +
"lastLogin = ?," +
"online = ?" +
" WHERE uid = ?;"
}
}