Improved psoserv locking strategy.

This commit is contained in:
Daan Vanden Bosch 2021-08-10 17:43:44 +02:00
parent f1a0de715f
commit 5af76bac7c
14 changed files with 132 additions and 164 deletions

2
.gitignore vendored
View File

@ -12,4 +12,4 @@ build
karma.config.generated.js karma.config.generated.js
# Config # Config
/psoserv/config.json /psoserv/*.conf

View File

@ -22,9 +22,9 @@ class Config(
val patch: PatchServerConfig? = null, val patch: PatchServerConfig? = null,
val auth: ServerConfig? = null, val auth: ServerConfig? = null,
val account: ServerConfig? = null, val account: ServerConfig? = null,
val proxy: ProxyConfig? = null,
val ships: List<ShipServerConfig> = emptyList(), val ships: List<ShipServerConfig> = emptyList(),
val blocks: List<BlockServerConfig> = emptyList(), val blocks: List<BlockServerConfig> = emptyList(),
val proxy: ProxyConfig? = null,
) )
@Serializable @Serializable

View File

@ -103,7 +103,7 @@ private fun initialize(config: Config, accountStore: AccountStore): List<Server>
for (blockCfg in config.blocks) { for (blockCfg in config.blocks) {
val block = BlockInfo( val block = BlockInfo(
name = validateName("Block", blockCfg.name) ?: "block_$blockI", name = validateName("Block", blockCfg.name) ?: "block_$blockI",
uiName = blockCfg.uiName ?: "BLOCK${blockI.toString(2).padStart(2, '0')}", uiName = blockCfg.uiName ?: "BLOCK${blockI.toString().padStart(2, '0')}",
bindPair = Inet4Pair(address, blockCfg.port ?: blockPort++), bindPair = Inet4Pair(address, blockCfg.port ?: blockPort++),
) )
blockI++ blockI++

View File

@ -1,5 +1,61 @@
package world.phantasmal.psoserv.data package world.phantasmal.psoserv.data
import mu.KLogger
class AccountData(
private val logger: KLogger,
account: Account,
playing: PlayingAccount?,
private val password: String,
private var loggedIn: Boolean,
) {
/**
* All access to this class' properties must synchronize on this lock.
*/
private val lock = Any()
private var _account = account
private var _playing = playing
val account: Account get() = synchronized(lock) { _account }
val playing: PlayingAccount? get() = synchronized(lock) { _playing }
init {
require(password.length <= 16)
}
fun logIn(password: String): LogInResult =
synchronized(lock) {
if (password != this.password) {
LogInResult.BadPassword
} else if (loggedIn) {
LogInResult.AlreadyLoggedIn
} else {
loggedIn = true
LogInResult.Ok
}
}
fun logOut() {
synchronized(lock) {
if (!loggedIn) {
logger.warn {
"""Trying to log out account ${account.id} "${account.username}" while it wasn't logged in."""
}
}
_playing = null
loggedIn = false
}
}
fun setPlaying(char: Character, blockId: Int) {
synchronized(lock) {
_playing = PlayingAccount(account, char, blockId)
loggedIn = true
}
}
}
class Account( class Account(
val id: Long, val id: Long,
val username: String, val username: String,
@ -45,3 +101,7 @@ enum class SectionId {
Yellowboze, Yellowboze,
Whitill, Whitill,
} }
enum class LogInResult {
Ok, BadPassword, AlreadyLoggedIn
}

View File

@ -3,22 +3,23 @@ package world.phantasmal.psoserv.data
import mu.KLogger import mu.KLogger
class AccountStore(private val logger: KLogger) { class AccountStore(private val logger: KLogger) {
/**
* All access to this class' properties must synchronize on this lock.
*/
private val lock = Any()
private var nextId: Long = 1L private var nextId: Long = 1L
private var nextGuildCardNo: Int = 1 private var nextGuildCardNo: Int = 1
/** private val idToAccountData = mutableMapOf<Long, AccountData>()
* Maps usernames to accounts. Accounts are created on the fly. private val usernameToAccountData = mutableMapOf<String, AccountData>()
*/
private val accounts = mutableMapOf<String, AccountData>()
/** fun getAccountData(username: String, password: String): AccountData =
* Logged in accounts must always be logged out with [logOut]. synchronized(lock) {
*/ // Simply create the account if it doesn't exist yet.
fun logIn(username: String, password: String): LogInResult = usernameToAccountData.getOrPut(username) {
synchronized(this) {
val data = accounts.getOrPut(username) {
val accountId = nextId++ val accountId = nextId++
AccountData( AccountData(
logger = logger,
account = Account( account = Account(
id = accountId, id = accountId,
username = username, username = username,
@ -38,74 +39,18 @@ class AccountStore(private val logger: KLogger) {
playing = null, playing = null,
password = password, password = password,
loggedIn = false, loggedIn = false,
) ).also {
} // Ensure it can also be found by ID.
idToAccountData[accountId] = it
if (password != data.password) {
LogInResult.BadPassword
} else if (data.loggedIn) {
LogInResult.AlreadyLoggedIn
} else {
data.loggedIn = true
LogInResult.Ok(data.account)
}
}
fun logOut(accountId: Long) {
synchronized(this) {
val data = accounts.values.find { it.account.id == accountId }
if (data == null) {
logger.warn {
"Trying to log out nonexistent account $accountId."
} }
} else {
if (!data.loggedIn) {
logger.warn {
"""Trying to log out account ${data.account.id} "${data.account.username}" while it wasn't logged in."""
}
}
data.playing = null
data.loggedIn = false
} }
} }
}
fun getAccountById(accountId: Long): Account? = fun getPlayingAccountsForBlock(blockId: Int): List<PlayingAccount> =
synchronized(this) { synchronized(lock) {
accounts.values.find { it.account.id == accountId }?.account idToAccountData.values.asSequence()
}
fun setAccountPlaying(accountId: Long, char: Character, blockId: Int): Account {
synchronized(this) {
val data = accounts.values.first { it.account.id == accountId }
data.playing = PlayingAccount(data.account, char, blockId)
return data.account
}
}
fun getAccountsByBlock(blockId: Int): List<PlayingAccount> =
synchronized(this) {
accounts.values
.filter { it.loggedIn && it.playing?.blockId == blockId }
.mapNotNull { it.playing } .mapNotNull { it.playing }
.filter { it.blockId == blockId }
.toList()
} }
sealed class LogInResult {
class Ok(val account: Account) : LogInResult()
object BadPassword : LogInResult()
object AlreadyLoggedIn : LogInResult()
}
private class AccountData(
var account: Account,
var playing: PlayingAccount?,
val password: String,
var loggedIn: Boolean,
) {
init {
require(password.length <= 16)
}
}
} }

View File

@ -57,6 +57,13 @@ object BbMessageDescriptor : MessageDescriptor<BbMessage> {
0x04EB -> BbMessage.GetFileList(buffer) 0x04EB -> BbMessage.GetFileList(buffer)
else -> BbMessage.Unknown(buffer) else -> BbMessage.Unknown(buffer)
} }
override fun createInitEncryption(serverKey: ByteArray, clientKey: ByteArray) =
BbMessage.InitEncryption(
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
serverKey,
clientKey,
)
} }
sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_SIZE) { sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_SIZE) {
@ -601,7 +608,7 @@ class GuildCardEntry(
val name: String, val name: String,
val description: String, val description: String,
val sectionId: Int, val sectionId: Int,
val characterClass: Int, val charClass: Int,
) )
class GuildCard( class GuildCard(

View File

@ -39,6 +39,8 @@ interface MessageDescriptor<out MessageType : Message> {
fun readHeader(buffer: Buffer): Header fun readHeader(buffer: Buffer): Header
fun readMessage(buffer: Buffer): MessageType fun readMessage(buffer: Buffer): MessageType
fun createInitEncryption(serverKey: ByteArray, clientKey: ByteArray): MessageType
} }
interface InitEncryptionMessage : Message { interface InitEncryptionMessage : Message {

View File

@ -37,6 +37,13 @@ object PcMessageDescriptor : MessageDescriptor<PcMessage> {
0x14 -> PcMessage.Redirect(buffer) 0x14 -> PcMessage.Redirect(buffer)
else -> PcMessage.Unknown(buffer) else -> PcMessage.Unknown(buffer)
} }
override fun createInitEncryption(serverKey: ByteArray, clientKey: ByteArray) =
PcMessage.InitEncryption(
"Patch Server. Copyright SonicTeam, LTD. 2001",
serverKey,
clientKey,
)
} }
sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_SIZE) { sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_SIZE) {

View File

@ -4,8 +4,9 @@ import world.phantasmal.core.math.clamp
import world.phantasmal.psolib.Endianness import world.phantasmal.psolib.Endianness
import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psolib.buffer.Buffer
import world.phantasmal.psolib.cursor.cursor import world.phantasmal.psolib.cursor.cursor
import world.phantasmal.psoserv.data.AccountData
import world.phantasmal.psoserv.data.AccountStore import world.phantasmal.psoserv.data.AccountStore
import world.phantasmal.psoserv.data.AccountStore.LogInResult import world.phantasmal.psoserv.data.LogInResult
import world.phantasmal.psoserv.encryption.BbCipher import world.phantasmal.psoserv.encryption.BbCipher
import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.encryption.Cipher
import world.phantasmal.psoserv.messages.* import world.phantasmal.psoserv.messages.*
@ -26,38 +27,23 @@ class AccountServer(
serverCipher: Cipher, serverCipher: Cipher,
clientCipher: Cipher, clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> { ): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
private var accountId: Long? = null private var accountData: AccountData? = null
private val guildCardBuffer = Buffer.withSize(54672) private val guildCardBuffer = Buffer.withSize(54672)
private var fileChunkNo = 0 private var fileChunkNo = 0
private var charSlot: Int = 0 private var charSlot: Int = 0
private var charSelected: Boolean = false private var charSelected: Boolean = false
init {
ctx.send(
BbMessage.InitEncryption(
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
serverCipher.key,
clientCipher.key,
),
encrypt = false,
)
}
override fun process(message: BbMessage): Boolean = when (message) { override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> { is BbMessage.Authenticate -> {
when ( val accountData = accountStore.getAccountData(message.username, message.password)
val result = accountStore.logIn( this.accountData = accountData
message.username,
message.password,
)
) {
is LogInResult.Ok -> {
val account = result.account
this.accountId = account.id
when (accountData.logIn(message.password)) {
LogInResult.Ok -> {
charSlot = message.charSlot charSlot = message.charSlot
charSelected = message.charSelected charSelected = message.charSelected
val account = accountData.account
ctx.send( ctx.send(
BbMessage.AuthData( BbMessage.AuthData(
AuthStatus.Success, AuthStatus.Success,
@ -102,7 +88,7 @@ class AccountServer(
} }
is BbMessage.GetAccount -> { is BbMessage.GetAccount -> {
accountId?.let(accountStore::getAccountById)?.let { accountData?.account?.let {
ctx.send(BbMessage.Account(it.guildCardNo, it.teamId)) ctx.send(BbMessage.Account(it.guildCardNo, it.teamId))
} }
@ -110,7 +96,7 @@ class AccountServer(
} }
is BbMessage.CharSelect -> { is BbMessage.CharSelect -> {
val account = accountId?.let(accountStore::getAccountById) val account = accountData?.account
if (account != null && message.slot in account.characters.indices) { if (account != null && message.slot in account.characters.indices) {
if (message.selected) { if (message.selected) {
@ -266,9 +252,9 @@ class AccountServer(
private fun logOut() { private fun logOut() {
try { try {
accountId?.let(accountStore::logOut) accountData?.let(AccountData::logOut)
} finally { } finally {
accountId = null accountData = null
} }
} }
} }

View File

@ -22,17 +22,6 @@ class AuthServer(
serverCipher: Cipher, serverCipher: Cipher,
clientCipher: Cipher, clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> { ): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
init {
ctx.send(
BbMessage.InitEncryption(
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
serverCipher.key,
clientCipher.key,
),
encrypt = false,
)
}
override fun process(message: BbMessage): Boolean = when (message) { override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> { is BbMessage.Authenticate -> {
// Don't actually authenticate, since we're simply redirecting the player to the // Don't actually authenticate, since we're simply redirecting the player to the

View File

@ -1,7 +1,8 @@
package world.phantasmal.psoserv.servers package world.phantasmal.psoserv.servers
import world.phantasmal.psoserv.data.AccountData
import world.phantasmal.psoserv.data.AccountStore import world.phantasmal.psoserv.data.AccountStore
import world.phantasmal.psoserv.data.AccountStore.LogInResult import world.phantasmal.psoserv.data.LogInResult
import world.phantasmal.psoserv.encryption.BbCipher import world.phantasmal.psoserv.encryption.BbCipher
import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.encryption.Cipher
import world.phantasmal.psoserv.messages.* import world.phantasmal.psoserv.messages.*
@ -22,27 +23,16 @@ class BlockServer(
serverCipher: Cipher, serverCipher: Cipher,
clientCipher: Cipher, clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> { ): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
private var accountId: Long? = null private var accountData: AccountData? = null
init {
ctx.send(
BbMessage.InitEncryption(
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
serverCipher.key,
clientCipher.key,
),
encrypt = false,
)
}
override fun process(message: BbMessage): Boolean = when (message) { override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> { is BbMessage.Authenticate -> {
when ( val accountData = accountStore.getAccountData(message.username, message.password)
val result = accountStore.logIn(message.username, message.password) this.accountData = accountData
) {
is LogInResult.Ok -> { when (accountData.logIn(message.password)) {
accountId = result.account.id LogInResult.Ok -> {
val char = result.account.characters.getOrNull(message.charSlot) val char = accountData.account.characters.getOrNull(message.charSlot)
if (char == null) { if (char == null) {
ctx.send( ctx.send(
@ -55,11 +45,8 @@ class BlockServer(
) )
) )
} else { } else {
val account = accountStore.setAccountPlaying( accountData.setPlaying(char, blockId)
result.account.id, val account = accountData.account
char,
blockId,
)
ctx.send( ctx.send(
BbMessage.AuthData( BbMessage.AuthData(
AuthStatus.Success, AuthStatus.Success,
@ -122,7 +109,7 @@ class BlockServer(
lobbyNo = 0u, lobbyNo = 0u,
blockNo = blockId.toUShort(), blockNo = blockId.toUShort(),
event = 0u, event = 0u,
players = accountStore.getAccountsByBlock(blockId).map { players = accountStore.getPlayingAccountsForBlock(blockId).map {
LobbyPlayer( LobbyPlayer(
playerTag = 0, playerTag = 0,
guildCardNo = it.account.guildCardNo, guildCardNo = it.account.guildCardNo,
@ -151,9 +138,9 @@ class BlockServer(
private fun logOut() { private fun logOut() {
try { try {
accountId?.let(accountStore::logOut) accountData?.let(AccountData::logOut)
} finally { } finally {
accountId = null accountData = null
} }
} }
} }

View File

@ -65,6 +65,13 @@ abstract class GameServer<MessageType : Message>(
override val readEncryptCipher: Cipher? = null override val readEncryptCipher: Cipher? = null
override val writeEncryptCipher: Cipher = serverCipher override val writeEncryptCipher: Cipher = serverCipher
init {
sendMessage(
messageDescriptor.createInitEncryption(serverCipher.key, clientCipher.key),
encrypt = false,
)
}
override fun processMessage(message: MessageType): ProcessResult = override fun processMessage(message: MessageType): ProcessResult =
if (receiver.process(message)) { if (receiver.process(message)) {
ProcessResult.Ok ProcessResult.Ok

View File

@ -19,17 +19,6 @@ class PatchServer(
serverCipher: Cipher, serverCipher: Cipher,
clientCipher: Cipher, clientCipher: Cipher,
): ClientReceiver<PcMessage> = object : ClientReceiver<PcMessage> { ): ClientReceiver<PcMessage> = object : ClientReceiver<PcMessage> {
init {
ctx.send(
PcMessage.InitEncryption(
"Patch Server. Copyright SonicTeam, LTD. 2001",
serverCipher.key,
clientCipher.key,
),
encrypt = false,
)
}
override fun process(message: PcMessage): Boolean = when (message) { override fun process(message: PcMessage): Boolean = when (message) {
is PcMessage.InitEncryption -> { is PcMessage.InitEncryption -> {
ctx.send(PcMessage.Login()) ctx.send(PcMessage.Login())

View File

@ -23,17 +23,6 @@ class ShipServer(
serverCipher: Cipher, serverCipher: Cipher,
clientCipher: Cipher, clientCipher: Cipher,
): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> { ): ClientReceiver<BbMessage> = object : ClientReceiver<BbMessage> {
init {
ctx.send(
BbMessage.InitEncryption(
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
serverCipher.key,
clientCipher.key,
),
encrypt = false,
)
}
override fun process(message: BbMessage): Boolean = when (message) { override fun process(message: BbMessage): Boolean = when (message) {
is BbMessage.Authenticate -> { is BbMessage.Authenticate -> {
// Don't actually authenticate, since we're simply letting the player choose a block // Don't actually authenticate, since we're simply letting the player choose a block