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
+1 -1
View File
@@ -19,7 +19,7 @@ import java.nio.ByteBuffer
object TestUtils {
fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE): Player {
val p = Player(PlayerDetails(name, name))
val p = Player(PlayerDetails(name))
p.details.session = MockSession()
p.ironmanManager.mode = ironman
Repository.addPlayer(p)
@@ -0,0 +1,55 @@
package core.auth
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import rs09.auth.DevelopmentAuthenticator
import rs09.auth.AuthResponse
import rs09.auth.UserAccountInfo
import rs09.storage.InMemoryStorageProvider
class DevelopmentAuthenticatorTests {
private val authProvider = DevelopmentAuthenticator()
private val storageProvider = InMemoryStorageProvider()
init {
authProvider.configureFor(storageProvider)
}
@Test fun shouldAllowCheckingIfAccountExists() {
val info = UserAccountInfo.createDefault()
info.username = "Billy"
Assertions.assertEquals(true, authProvider.canCreateAccountWith(info))
authProvider.createAccountWith(info)
Assertions.assertEquals(false, authProvider.canCreateAccountWith(info))
}
@Test fun loginWithValidAccountInfoReturnsSuccess() {
val info = UserAccountInfo.createDefault()
info.username = "Billy"
authProvider.createAccountWith(info)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Billy", "").first)
}
//Development authenticator should work regardless if account exists or not.
@Test fun loginWithInvalidAccountInfoReturnsSuccess() {
val info = UserAccountInfo.createDefault()
info.username = "Billy"
authProvider.createAccountWith(info)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Bilbo", "ebbeb").first)
}
@Test fun loginUsernameIsNotCaseSensitive() {
val info = UserAccountInfo.createDefault()
info.username = "Billy"
authProvider.createAccountWith(info)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Billy", "").first)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("billy", "").first)
}
//Development authenticator should basically bypass needing/creating an account entirely. useful for SP too.
@Test fun loginToUnregisteredAccountCreatesIt() {
authProvider.checkLogin("masterbaggins", "whatever")
val info = storageProvider.getAccountInfo("masterbaggins")
Assertions.assertEquals(2, info.rights)
}
}
@@ -0,0 +1,62 @@
package core.auth
import org.junit.Assert
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import rs09.auth.AuthResponse
import rs09.auth.ProductionAuthenticator
import rs09.auth.UserAccountInfo
import rs09.storage.InMemoryStorageProvider
class ProductionAuthenticatorTests {
companion object {
private val authProvider = ProductionAuthenticator()
private val storageProvider = InMemoryStorageProvider()
init {
authProvider.configureFor(storageProvider)
}
@BeforeAll @JvmStatic fun createTestAccount() {
val details = UserAccountInfo.createDefault()
details.username = "test"
details.password = "testing"
if(!storageProvider.checkUsernameTaken("test")) {
authProvider.createAccountWith(details)
}
}
}
@Test fun shouldRejectLoginWithInvalidDetails() {
Assertions.assertEquals(AuthResponse.InvalidCredentials, authProvider.checkLogin("test", "test2").first)
}
@Test fun loginUsernameIsNotCaseSensitive() {
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Test", "testing").first)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("test", "testing").first)
}
@Test fun shouldHashPasswords() {
Assertions.assertNotEquals("testing", storageProvider.getAccountInfo("test").password)
}
@Test fun shouldNotAllowBannedLogin() {
val info = storageProvider.getAccountInfo("test")
info.banEndTime = System.currentTimeMillis() + 1000L
storageProvider.update(info)
Assertions.assertEquals(AuthResponse.AccountDisabled, authProvider.checkLogin("test", "testing").first)
info.banEndTime = 0L
storageProvider.update(info)
Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("test", "testing").first)
}
@Test fun shouldNotAllowAlreadyOnlineLogin() {
val info = storageProvider.getAccountInfo("test")
info.online = true
storageProvider.update(info)
Assertions.assertEquals(AuthResponse.AlreadyOnline, authProvider.checkLogin("test", "testing").first)
info.online = false
storageProvider.update(info)
}
}
@@ -0,0 +1,40 @@
package core.net
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import rs09.auth.AuthResponse
import rs09.net.packet.`in`.Login
import java.nio.ByteBuffer
class LoginTests {
@Test fun shouldDecodeCorrectlyEncodedPacket() {
val localCopy = validLoginPacket.copyOf(validLoginPacket.size)
val loginInfo = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy))
Assertions.assertEquals(AuthResponse.Success, loginInfo.first, "Info: ${loginInfo.second}")
}
@Test fun shouldNeverThrowExceptionButReturnUnexpectedErrorResponseInstead() {
val localCopy = validLoginPacket.copyOf(validLoginPacket.size)
for(i in 15 until validLoginPacket.size) {
localCopy[i] = 0 //corrupt some data on purpose
}
var response: AuthResponse = AuthResponse.Updating
Assertions.assertDoesNotThrow {
val (res, _) = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy))
response = res
}
Assertions.assertEquals(AuthResponse.UnexpectedError, response)
}
@Test fun loginPacketWithInvalidOpcodeShouldReturnAHelpfulResponse() {
val localCopy = validLoginPacket.copyOf(validLoginPacket.size)
localCopy[0] = 2 //set to invalid login opcode
val (response, _) = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy))
Assertions.assertEquals(AuthResponse.InvalidLoginServer, response)
}
companion object {
val validLoginPacket = byteArrayOf(16, 1, 80, 0, 0, 2, 18, 0, 1, 1, 2, 2, -3, 1, -9, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 107, 75, 109, 111, 107, 51, 107, 74, 113, 79, 101, 78, 54, 68, 51, 109, 68, 100, 105, 104, 99, 111, 51, 111, 80, 101, 89, 78, 50, 75, 70, 121, 54, 87, 53, 45, 45, 118, 90, 85, 98, 78, 65, 0, 0, 0, 0, 0, 1, 89, -73, -29, 0, 0, -47, -49, 34, 92, -124, 110, -97, 46, -33, -18, 122, 8, 76, 93, -82, 16, 53, 23, -113, -73, 101, 126, -93, 125, -65, 115, -59, 19, 90, -54, -102, -76, -99, -5, -68, -51, 95, -20, -27, 10, 60, 60, 108, 5, 76, 9, -63, 97, 106, 74, 116, 0, 58, 0, 75, -111, -128, -34, 12, 64, 47, -92, -33, -120, -109, -7, 23, 124, 122, 40, -107, 56, -34, -93, 64, -82, 58, -90, 7, 127, -15, -85, 125, 43, 15, 0, 112, 60, 4, 75, 72, 55, 18, 83, -119, -39, 32, -113, 21, 104, -49, 66, -102, 104, -13, 32, 117, -106, 94, -30, 37, -56, -67, 21, 77, 70, -128, 113, 86, 84, 83, 115, 3, 55, -106, 127, -14, -15, 6, 42, -92, -56, 114, 24, 83, 32, -127, 78, 14, -98, -30, -38, -115, 9, -106, 120, 101, -117, -50, -40, -64, -50, -106, -98, 5, -86, 28, -127, -5, 109, -107, 49, -107, 63, 81, -81, -109, -21, 25, -68, 63, 102, -5, 8, -96, 126, -128, -116, -32, 26, 76, 54, -63, -37, 41, 57, 65, -53, -22, 83, 61, -128, -17, -21, 87, 76, -8, -95, -45, -80, -60, -62, 96, 49, 26, 49, 94, 80, 11, -12, -95, -58, -116, -54, -107, 91, 104, 58, 33, 20, -93, -68, -83, -116, 63, -18, 36, 30, -98, 77, -107, 122, 79, -27, 67, -94, 125, -81, -21, -75, -71, -45, -39, 112, 40)
}
}
@@ -0,0 +1,181 @@
package core.storage
import core.auth.ProductionAuthenticatorTests
import org.junit.After
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import rs09.auth.UserAccountInfo
import rs09.game.system.SystemLogger
import rs09.storage.SQLStorageProvider
import java.sql.SQLDataException
class SQLStorageProviderTests {
companion object {
val storage = SQLStorageProvider()
val testAccountNames = ArrayList<String>()
init {
storage.configure("localhost", "global", "root", "")
val details = UserAccountInfo.createDefault()
details.rights = 2
details.username = "test"
}
@AfterAll @JvmStatic fun cleanup() {
SystemLogger.logInfo("Cleaning up unit test accounts")
testAccountNames.forEach {name ->
SystemLogger.logInfo("Removing test account $name")
val info = UserAccountInfo.createDefault()
info.username = name
storage.remove(info)
}
}
}
@Test
fun shouldReturnTrueIfUsernameExists() {
val data = UserAccountInfo.createDefault()
data.username = "test123123"
data.password = "test"
testAccountNames.add("test123123")
storage.store(data)
val exists = storage.checkUsernameTaken("test")
Assertions.assertEquals(true, exists)
}
@Test fun shouldReturnCorrectUserData() {
val data = UserAccountInfo.createDefault()
data.username = "test111"
data.password = "test"
data.rights = 2
testAccountNames.add("test111")
storage.store(data)
val accountInfo = storage.getAccountInfo("test111")
Assertions.assertEquals(2, accountInfo.rights)
}
@Test fun shouldAllowStoreUserData() {
val userData = UserAccountInfo.createDefault()
userData.username = "storageTest"
userData.password = "test"
testAccountNames.add("storageTest")
storage.store(userData)
val exists = storage.checkUsernameTaken("storageTest")
Assertions.assertEquals(true, exists)
}
@Test fun shouldNotAllowDuplicateAccountStorage() {
val userData = UserAccountInfo.createDefault()
userData.username = "bilbo111"
userData.password = "test"
testAccountNames.add("bilbo111")
storage.store(userData)
Assertions.assertThrows(SQLDataException::class.java) {
storage.store(userData)
}
}
@Test fun shouldAllowRemoveUserInfo() {
val userData = UserAccountInfo.createDefault()
userData.username = "bepis"
userData.password = "test"
testAccountNames.add("bepis")
storage.store(userData)
storage.remove(userData)
Assertions.assertEquals(false, storage.checkUsernameTaken("bepis"))
}
@Test fun shouldUpdateUserData() {
val userData = UserAccountInfo.createDefault()
userData.username = "borpis"
userData.password = "test"
testAccountNames.add("borpis")
storage.store(userData)
userData.credits = 2
storage.update(userData)
val data = storage.getAccountInfo(userData.username)
Assertions.assertEquals(2, data.credits, "Wrong data: $data")
}
@Test fun shouldNotAllowStoreOrUpdateEmptyData() {
val info = UserAccountInfo.createDefault()
Assertions.assertThrows(IllegalStateException::class.java) {
storage.store(info)
}
Assertions.assertThrows(IllegalStateException::class.java) {
storage.update(info)
}
info.username = "test"
Assertions.assertThrows(IllegalStateException::class.java) {
storage.store(info)
}
Assertions.assertThrows(IllegalStateException::class.java) {
storage.update(info)
}
}
@Test fun shouldSetDefaultValuesWhenDBFieldIsNull() {
val defaultData = UserAccountInfo.createDefault()
//manually insert a definitely-mostly-null entry into the DB
val conn = storage.getConnection()
conn.use {
val stmt = conn.prepareStatement("INSERT INTO members (username) VALUES (?);")
stmt.setString(1, "nulltestacc")
testAccountNames.add("nulltestacc")
stmt.execute()
}
var data: UserAccountInfo = UserAccountInfo.createDefault()
Assertions.assertDoesNotThrow {
data = storage.getAccountInfo("nulltestacc")
}
Assertions.assertEquals(defaultData.password, data.password)
Assertions.assertEquals(defaultData.rights, data.rights)
Assertions.assertEquals(defaultData.credits, data.credits)
Assertions.assertEquals(defaultData.ip, data.ip)
Assertions.assertEquals(defaultData.lastUsedIp, data.lastUsedIp)
Assertions.assertEquals(defaultData.muteEndTime, data.muteEndTime)
Assertions.assertEquals(defaultData.banEndTime, data.banEndTime)
Assertions.assertEquals(defaultData.contacts, data.contacts)
Assertions.assertEquals(defaultData.blocked, data.blocked)
Assertions.assertEquals(defaultData.clanName, data.clanName)
Assertions.assertEquals(defaultData.currentClan, data.currentClan)
Assertions.assertEquals(defaultData.clanReqs, data.clanReqs)
Assertions.assertEquals(defaultData.timePlayed, data.timePlayed)
Assertions.assertEquals(defaultData.lastLogin, data.lastLogin)
Assertions.assertEquals(defaultData.online, data.online)
}
@Test fun updatingPropertiesOnTheDatabaseEndShouldBePreservedWhenFetchingAccountInfo() {
val conn = storage.getConnection()
conn.use {
val stmt = conn.prepareStatement("INSERT INTO members (username) VALUES (?);")
stmt.setString(1, "dbupdateacc")
testAccountNames.add("dbupdateacc")
stmt.execute()
stmt.close()
val stmt2 = conn.prepareStatement("UPDATE members SET rights = 2 WHERE username = \"dbupdateacc\";")
stmt2.execute()
}
val info = storage.getAccountInfo("dbupdateacc")
Assertions.assertEquals(2, info.rights)
info.rights = 1
storage.update(info)
val info2 = storage.getAccountInfo("dbupdateacc")
Assertions.assertEquals(1, info2.rights)
val conn2 = storage.getConnection()
conn2.use {
val stmt = conn2.prepareStatement("UPDATE members SET rights = 2 WHERE username = \"dbupdateacc\";")
stmt.execute()
}
val info3 = storage.getAccountInfo("dbupdateacc")
Assertions.assertEquals(2, info3.rights)
}
}