Skip to content

Commit

Permalink
prevent duplicate connections
Browse files Browse the repository at this point in the history
  • Loading branch information
hex-agon committed Sep 3, 2023
1 parent dc2344c commit f7566da
Show file tree
Hide file tree
Showing 14 changed files with 97 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class GameServer(

fun registerPlayer(
channel: Channel,
sessionInfo: SessionClient.SessionInfo,
sessionInfo: SessionClient.HandoverInfo,
playerWallet: PlayerWallet,
characterRoster: CharacterRoster,
caddieRoster: CaddieRoster,
Expand Down Expand Up @@ -86,6 +86,7 @@ class GameServer(
player.currentRoom?.removePlayer(player)
player.currentChannel?.removePlayer(player)
players.remove(player)
sessionClient.unregisterSession(player)
LOGGER.info("{} disconnected", player.nickname)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.lettuce.core.RedisClient
import io.lettuce.core.api.sync.RedisCommands
import work.fking.pangya.game.player.Player

class SessionClient(
private val redisCommands: RedisCommands<String, String>
private val redis: RedisCommands<String, String>
) {
constructor(redisClient: RedisClient) : this(redisClient.connect().sync())

private val objectMapper = jacksonObjectMapper()

fun loadSession(sessionKey: String): SessionInfo? {
val json = redisCommands[sessionKey] ?: return null
return objectMapper.readValue<SessionInfo>(json)
fun unregisterSession(player: Player) {
redis.del("session-${player.username}")
}

fun loadHandoverInfo(loginKey: String): HandoverInfo? {
val json = redis[loginKey] ?: return null
return objectMapper.readValue<HandoverInfo>(json)
}

@JvmRecord
data class SessionInfo(
data class HandoverInfo(
val targetServerId: Int,
val sessionKey: String,
val loginKey: String,
val uid: Int,
val username: String,
val nickname: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class HandoverHandler(
buffer.readIntLE()
val sessionKey = buffer.readPString()
LOGGER.info("Successful handover username={}, uid={}, clientVersion={}, loginKey={}, sessionKey={}", username, uid, clientVersion, loginKey, sessionKey)
gameServer.runTask(HandoverTask(gameServer, ctx.channel(), cryptKey, sessionKey))
gameServer.runTask(HandoverTask(gameServer, ctx.channel(), cryptKey, loginKey))
}

override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ class HandoverTask(
private val gameServer: GameServer,
private val channel: Channel,
private val cryptKey: Int,
private val sessionKey: String
private val loginKey: String
) : Runnable {

override fun run() {
val sessionInfo: SessionClient.SessionInfo? = try {
gameServer.sessionClient.loadSession(sessionKey)
val sessionInfo: SessionClient.HandoverInfo? = try {
gameServer.sessionClient.loadHandoverInfo(loginKey)
} catch (e: Exception) {
LOGGER.warn("Handover error sessionKey={}, message={}", sessionKey, e.message)
LOGGER.warn("Handover error loginKey={}, message={}", loginKey, e.message)
channel.writeAndFlush(HandoverReplies.error(HandoverResult.CANNOT_CONNECT_LOGIN_SERVER))
return
}
if (sessionInfo == null) {
LOGGER.warn("Handover failed, unknown sessionKey={}", sessionKey)
LOGGER.warn("Handover failed, unknown loginKey={}", loginKey)
channel.writeAndFlush(HandoverReplies.error(HandoverResult.CANNOT_CONNECT_LOGIN_SERVER))
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import work.fking.pangya.discovery.DiscoveryClient
import work.fking.pangya.discovery.HeartbeatPublisher
import work.fking.pangya.discovery.ServerType.LOGIN
import work.fking.pangya.login.auth.DatabaseAuthenticator
import work.fking.pangya.login.auth.NOOP_AUTHENTICATOR
import work.fking.pangya.login.auth.SessionClient
import work.fking.pangya.login.session.SessionClient
import work.fking.pangya.login.persistence.AccountRepository
import java.nio.file.Files
import java.nio.file.Path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import org.slf4j.LoggerFactory
import work.fking.pangya.discovery.DiscoveryClient
import work.fking.pangya.discovery.ServerType
import work.fking.pangya.login.auth.Authenticator
import work.fking.pangya.login.auth.SessionClient
import work.fking.pangya.login.auth.UserInfo
import work.fking.pangya.login.net.LoginState.HANDED_OVER
import work.fking.pangya.login.net.pipe.ServerChannelInitializer
import work.fking.pangya.login.packet.outbound.LoginReplies
import work.fking.pangya.login.packet.outbound.ServerListReplies
import work.fking.pangya.login.session.SessionClient
import work.fking.pangya.networking.selectBestEventLoopAvailable
import work.fking.pangya.networking.selectBestServerChannelAvailable
import java.net.InetAddress
Expand Down Expand Up @@ -56,7 +57,11 @@ class LoginServer(

private fun onPlayerDisconnect(player: Player) {
LOGGER.info("{} disconnected", player.username)
sessionClient.unregisterSession(player)

if (player.state != HANDED_OVER) {
sessionClient.unregisterSession(player)
}
sessionClient.expireHandoverInfo(player)
}

fun start() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ enum class LoginState {
/**
* Player is fully authenticated, has a valid nickname and has a base character.
*/
LOGGED_IN;
LOGGED_IN,

/**
* Player has been handed over to a game server.
*/
HANDED_OVER;

fun validTransition(to: LoginState): Boolean {
return when (this) {
Expand All @@ -33,7 +38,8 @@ enum class LoginState {
SELECTING_NICKNAME -> to == SELECTED_NICKNAME
SELECTED_NICKNAME -> to == LOGGED_IN || to == SELECTING_CHARACTER
SELECTING_CHARACTER -> to == SELECTING_NICKNAME || to == LOGGED_IN
LOGGED_IN -> false
LOGGED_IN -> to == HANDED_OVER
HANDED_OVER -> false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.netty.handler.timeout.ReadTimeoutException
import org.slf4j.LoggerFactory
import work.fking.pangya.login.LoginServer
import work.fking.pangya.login.packet.outbound.LoginReplies
import work.fking.pangya.login.packet.outbound.LoginReplies.Error.INCORRECT_USERNAME_PASSWORD
import work.fking.pangya.login.task.LoginTask
import work.fking.pangya.networking.crypt.PangCrypt
import work.fking.pangya.networking.crypt.PangCrypt.PangCryptException
Expand Down Expand Up @@ -54,7 +55,7 @@ class LoginHandler(
val loginKey = buffer.readPString()

LOGGER.debug("Rejecting reconnect for username={} uid={} loginKey={}", username, playerUid, loginKey)
ctx.writeAndFlush(LoginReplies.error(LoginReplies.Error.INCORRECT_USERNAME_PASSWORD))
ctx.writeAndFlush(LoginReplies.error(INCORRECT_USERNAME_PASSWORD))
}

override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ object LoginReplies {
}
}

// TODO: We're probably doing something wrong here because it doesn't match any of the documented stuff
fun error(error: Error, message: String = ""): OutboundPacket {
return OutboundPacket { buffer: ByteBuf ->
buffer.writeShortLE(RESULT_PACKET_ID)
Expand Down Expand Up @@ -86,7 +87,6 @@ object LoginReplies {
INCORRECT_SSN(0xC),
INCORRECT_USERNAME(0xD),
WHITELISTED_USERS_ONLY(0xE),
SERVER_MAINTENANCE(0xF),
GEO_BLOCKED(0x10)
SERVER_MAINTENANCE(0xF)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package work.fking.pangya.login.session

data class HandoverInfo(
val targetServerId: Int,
val loginKey: String,
val uid: Int,
val username: String,
val nickname: String,
val characterIffId: Int?,
val characterHairColor: Int?
)
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
package work.fking.pangya.login.auth
package work.fking.pangya.login.session

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.lettuce.core.RedisClient
import io.lettuce.core.api.sync.RedisCommands
import work.fking.pangya.login.Player

class SessionClient(
private val redisCommands: RedisCommands<String, String>
private val redis: RedisCommands<String, String>
) {
private val objectMapper = jacksonObjectMapper()

constructor(redisClient: RedisClient) : this(redisClient.connect().sync())

fun registerSession(player: Player, serverId: Int) {
fun sessionKeyForUsername(username: String): String? {
return redis["session-$username"]
}

fun registerSession(player: Player) {
redis["session-${player.username}"] = player.sessionKey
}

fun unregisterSession(player: Player) {
redis.del("session-${player.username}")
}

fun registerHandoverInfo(player: Player, serverId: Int) {
val nickname = player.nickname
requireNotNull(nickname) { "Cannot register session for ${player.username} because it doesn't have a nickname set" }

val userInfo = SessionInfo(
val userInfo = HandoverInfo(
targetServerId = serverId,
sessionKey = player.sessionKey,
loginKey = player.loginKey,
uid = player.uid,
username = player.username,
nickname = nickname,
characterIffId = player.pickedCharacterIffId,
characterHairColor = player.pickedCharacterHairColor
)
redisCommands[player.sessionKey] = objectMapper.writeValueAsString(userInfo)
redis[player.loginKey] = objectMapper.writeValueAsString(userInfo)
}

fun unregisterSession(player: Player) {
fun expireHandoverInfo(player: Player) {
// This is called once the player disconnects from the login server, then,
// the game server has around 10 seconds to retrieve the session before it expires
redisCommands.expire(player.sessionKey, 10)
redis.expire(player.loginKey, 10)
}

private data class SessionInfo(
val targetServerId: Int,
val sessionKey: String,
val uid: Int,
val username: String,
val nickname: String,
val characterIffId: Int?,
val characterHairColor: Int?
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package work.fking.pangya.login.session

import java.time.ZonedDateTime

data class SessionInfo(
val key: String,
val createdAt: ZonedDateTime = ZonedDateTime.now()
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package work.fking.pangya.login.task
import org.slf4j.LoggerFactory
import work.fking.pangya.login.LoginServer
import work.fking.pangya.login.Player
import work.fking.pangya.login.net.LoginState
import work.fking.pangya.login.packet.outbound.LoginReplies

private val LOGGER = LoggerFactory.getLogger(HandoverTask::class.java)
Expand All @@ -15,7 +16,8 @@ class HandoverTask(

override fun run() {
LOGGER.info("Player {} is being handed over to serverId={} with loginKey={} and sessionKey={}", player.uid, serverId, player.loginKey, player.sessionKey)
runCatching { server.sessionClient.registerSession(player, serverId) }.onFailure { throw RuntimeException("Failed to register player session") }
runCatching { server.sessionClient.registerHandoverInfo(player, serverId) }.onFailure { throw RuntimeException("Failed to register player session") }
player.writeAndFlush(LoginReplies.sessionKey(player.sessionKey))
player.state = LoginState.HANDED_OVER
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import work.fking.pangya.login.net.LoginState.SELECTING_CHARACTER
import work.fking.pangya.login.net.LoginState.SELECTING_NICKNAME
import work.fking.pangya.login.net.pipe.ProtocolDecoder
import work.fking.pangya.login.packet.outbound.LoginReplies
import work.fking.pangya.login.packet.outbound.LoginReplies.Error.INCORRECT_USERNAME_PASSWORD
import work.fking.pangya.login.packet.outbound.LoginReplies.Error.INVALID_ID

private val PROTOCOL: ClientProtocol = ClientProtocol(ClientPacketType.values())
private val PROTOCOL: ClientProtocol = ClientProtocol(ClientPacketType.entries.toTypedArray())
private val LOGGER = LoggerFactory.getLogger(LoginTask::class.java)

class LoginTask(
Expand All @@ -38,7 +40,15 @@ class LoginTask(
}

private fun onSuccessAuth(userInfo: UserInfo) {
val sessionClient = loginServer.sessionClient
val userSession = sessionClient.sessionKeyForUsername(username)

if (userSession == null) {
duplicateConnection()
return
}
val player = loginServer.registerPlayer(channel, userInfo)
sessionClient.registerSession(player)

val pipeline = channel.pipeline()
pipeline.remove("loginHandler")
Expand All @@ -61,6 +71,10 @@ class LoginTask(
}

private fun onInvalidCredentials() {
channel.writeAndFlush(LoginReplies.error(LoginReplies.Error.INCORRECT_USERNAME_PASSWORD))
channel.writeAndFlush(LoginReplies.error(INCORRECT_USERNAME_PASSWORD))
}

private fun duplicateConnection() {
channel.writeAndFlush(LoginReplies.error(INVALID_ID, "Account is already logged in."))
}
}

0 comments on commit f7566da

Please sign in to comment.