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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user