aded an early test to determine if player account database is active (#1281)

This commit is contained in:
Fate-JH 2025-07-31 01:12:28 -04:00
parent c416ba11df
commit b8a47016da

View file

@ -24,7 +24,6 @@ import org.joda.time.LocalDateTime
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.matching.Regex
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
object LoginActor { object LoginActor {
@ -33,94 +32,31 @@ object LoginActor {
private case object UpdateServerList extends Command private case object UpdateServerList extends Command
final case class ReceptionistListing(listing: Receptionist.Listing) extends Command final case class ReceptionistListing(listing: Receptionist.Listing) extends Command
}
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) /**
extends Actor * What does a token do?
with MDCContextAware { * No one knows.
* @return a 32-bit ascii string
import scala.concurrent.ExecutionContext.Implicits.global */
private def generateToken(): String = {
val usernameRegex: Regex = """[A-Za-z0-9]{3,}""".r val r = new scala.util.Random
val sb = new mutable.StringBuilder
var leftRef: ActorRef = Default.Actor for (_ <- 1 to 31) {
var rightRef: ActorRef = Default.Actor sb.append(r.nextPrintableChar())
var accountIntermediary: ActorRef = Default.Actor
var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor
var updateServerListTask: Cancellable = Default.Cancellable
var ipAddress: String = ""
var hostName: String = ""
var canonicalHostName: String = ""
var port: Int = 0
val serverName: String = Config.app.world.serverName
val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
private val bcryptRounds = 12
ServiceManager.serviceManager ! Lookup("accountIntermediary")
ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self)
override def postStop(): Unit = {
if (updateServerListTask != null)
updateServerListTask.cancel()
}
def receive: Receive = {
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
case SocketPane.SocketPaneKey.Listing(listings) =>
sockets = listings.head
case ReceiveIPAddress(address) =>
ipAddress = address.Address
hostName = address.HostName
canonicalHostName = address.CanonicalHostName
port = address.Port
case LoginActor.UpdateServerList =>
updateServerList()
case packet: PlanetSideGamePacket =>
handleGamePkt(packet)
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case default =>
failWithError(s"Invalid packet class received: $default")
}
def handleGamePkt(pkt: PlanetSideGamePacket): Unit =
pkt match {
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) =>
// TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
if (token.isDefined)
log.debug(s"New login UN:$username Token:${token.get}. $clientVersion")
else {
log.debug(s"New login UN:$username. $clientVersion")
}
requestAccountLogin(username, password, token)
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) =>
log.info(s"Request to connect to world '$name' ...")
sockets ! SocketPane.GetNextPort("world", context.self)
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
} }
sb.toString
}
// generates a password from username and password combination /**
// mimics the process the launcher follows and hashes the password salted by the username * Generates a new password from username and password combination,
def generateNewPassword(username: String, password: String): String = { * hashing the initial password when salted by the username,
* mimicking the process the launcher follows.
* @param username part of the original details
* @param password part of the original details
* @param rounds number of times cryptographic mutation occurs
* @return new password
*/
private def generateNewPassword(username: String, password: String, rounds: Int): String = {
// salt password hash with username (like the launcher does) (username + password) // salt password hash with username (like the launcher does) (username + password)
val saltedPassword = username.concat(password) val saltedPassword = username.concat(password)
// https://stackoverflow.com/a/46332228 // https://stackoverflow.com/a/46332228
@ -129,20 +65,178 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
.digest(saltedPassword.getBytes("UTF-8")) .digest(saltedPassword.getBytes("UTF-8"))
.map("%02x".format(_)).mkString .map("%02x".format(_)).mkString
// bcrypt hash for DB storage // bcrypt hash for DB storage
val bcryptedPassword = hashedPassword.bcryptBounded(bcryptRounds) val bcryptedPassword = hashedPassword.bcryptBounded(rounds)
bcryptedPassword bcryptedPassword
} }
def requestAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = { /**
tokenOpt match { * Remove flavor from the server name that should not show up in the log.
case Some(token) => accountLoginWithToken(token) * @param name original name
case None => accountLogin(username, passwordOpt.getOrElse("")) * @return sanitized name
*/
private def sanitizeServerName(name: String): String = {
//remove color codes from the server name - look for '\\#' followed by six characters or numbers
name.replaceAll("\\\\#[\\da-fA-F]{6}","")
}
}
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
extends Actor
with MDCContextAware {
import scala.concurrent.ExecutionContext.Implicits.global
//private val usernameRegex: Regex = """[A-Za-z\d]{3,}""".r might be useful one day
private var accountIntermediary: ActorRef = Default.Actor
private var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor
private var updateServerListTask: Cancellable = Default.Cancellable
private var ipAddress: String = ""
private var hostName: String = ""
private var canonicalHostName: String = ""
private var port: Int = 0
private val serverName: String = Config.app.world.serverName
private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
private val bcryptRounds = 12
override def preStart(): Unit = {
super.preStart()
ServiceManager.serviceManager ! Lookup("accountIntermediary")
ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self)
}
override def postStop(): Unit = {
if (updateServerListTask != null)
updateServerListTask.cancel()
}
def receive: Receive = beforeLoginBehavior
private def persistentSetupMixinBehavior: Receive = {
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
case SocketPane.SocketPaneKey.Listing(listings) =>
sockets = listings.head
}
private def idlingBehavior: Receive = persistentSetupMixinBehavior.orElse {
case _ => ()
}
private def beforeLoginBehavior: Receive = persistentSetupMixinBehavior.orElse {
case ReceiveIPAddress(address) =>
ipAddress = address.Address
hostName = address.HostName
canonicalHostName = address.CanonicalHostName
port = address.Port
context.become(idlingBehavior)
runLoginTest()
case _ => ()
}
private def accountLoginBehavior: Receive = persistentSetupMixinBehavior.orElse {
case packet: PlanetSideGamePacket =>
handleGamePktDuringLogin(packet)
case default =>
failWithError(s"Invalid packet class received: $default")
}
private def displayingServerListBehavior: Receive = persistentSetupMixinBehavior.orElse {
case packet: PlanetSideGamePacket =>
handleGamePktDuringWorldSelect(packet)
case LoginActor.UpdateServerList =>
updateServerList()
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
context.become(idlingBehavior)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case default =>
failWithError(s"Invalid packet class received: $default")
}
private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse {
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
context.become(idlingBehavior)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case _ => ()
}
private def handleGamePktDuringLogin(pkt: PlanetSideGamePacket): Unit = {
pkt match {
case LoginMessage(majorVersion, minorVersion, buildDate, username, _, Some(token), revision) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.debug(s"New login UN:$username Token:$token. $clientVersion")
context.become(idlingBehavior)
accountLoginWithToken(token)
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, None, revision) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.debug(s"New login UN:$username. $clientVersion")
context.become(idlingBehavior)
accountLogin(username, password.getOrElse(""))
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
} }
} }
def accountLogin(username: String, password: String): Unit = { private def handleGamePktDuringWorldSelect(pkt: PlanetSideGamePacket): Unit = {
pkt match {
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) =>
val sanitizedName = LoginActor.sanitizeServerName(name)
log.info(s"Request to connect to world '$sanitizedName' ...")
context.become(waitingForServerTransferBehavior)
sockets ! SocketPane.GetNextPort("world", context.self)
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
}
}
private def runLoginTest(): Unit = {
import ctx._ import ctx._
val newToken = this.generateToken() val result = for {
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift("PSForever")))
accountOption <- accountsExact.headOption match {
case Some(account) =>
Future.successful(Some(account))
case None =>
Future.successful(None)
}
} yield accountOption
result.onComplete {
case Success(Some(_)) =>
context.become(accountLoginBehavior) // account found
case Success(None) =>
middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Character database not found; stopping ..."))
middlewareActor ! MiddlewareActor.Close()
case Failure(e) =>
log.error(e.getMessage)
middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Encountered login error; stopping ..."))
middlewareActor ! MiddlewareActor.Close()
}
}
private def accountLogin(username: String, password: String): Unit = {
import ctx._
val newToken = LoginActor.generateToken()
val result = for { val result = for {
// backwards compatibility: prefer exact match first, then try lowercase // backwards compatibility: prefer exact match first, then try lowercase
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username))) accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username)))
@ -160,7 +254,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
case None => case None =>
if (Config.app.login.createMissingAccounts) { if (Config.app.login.createMissingAccounts) {
// generate bcrypted passwords // generate bcrypted passwords
val bcryptedPassword = generateNewPassword(username, password) val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds)
val passhash = password.bcryptBounded(bcryptRounds) val passhash = password.bcryptBounded(bcryptRounds)
// save bcrypted password hash to DB // save bcrypted password hash to DB
ctx.run( ctx.run(
@ -201,7 +295,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
if (account.password == "") { if (account.password == "") {
// generate bcrypted password // generate bcrypted password
// use username as provided by the user (db entry could be wrong), that is the way the launcher does it // use username as provided by the user (db entry could be wrong), that is the way the launcher does it
val bcryptedPassword = generateNewPassword(username, password) val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds)
// update account, set password // update account, set password
ctx.run( ctx.run(
query[persistence.Account] query[persistence.Account]
@ -210,31 +304,33 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
loginSuccessfulResponse(username, newToken) loginSuccessfulResponse(username, newToken)
context.become(displayingServerListBehavior)
updateServerListTask = updateServerListTask =
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList)
future future
case (_, false) => case (_, false) =>
loginPwdFailureResponse(username, newToken) loginFailurePasswordResponse(username, newToken)
Future.successful(None) loginFailureAction()
case (true, _) => case (true, _) =>
loginAccountFailureResponse(username, newToken) loginAccountFailureResponse(username, newToken)
Future.successful(None) loginFailureAction()
} }
case None => Future.successful(None) case None =>
loginFailureAction()
} }
} yield login } yield login
result.onComplete { result.onComplete {
case Success(_) => case Success(_) => ()
case Failure(e) => log.error(e.getMessage) case Failure(e) => log.error(e.getMessage)
} }
} }
def accountLoginWithToken(token: String): Unit = { private def accountLoginWithToken(token: String): Unit = {
import ctx._ import ctx._
val newToken = this.generateToken() val newToken = LoginActor.generateToken()
val result = for { val result = for {
accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token))) accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token)))
accountOption <- accountsExact.headOption match { accountOption <- accountsExact.headOption match {
@ -267,33 +363,35 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
_.port -> lift(port) _.port -> lift(port)
) )
) )
loginSuccessfulResponseToken(account.username, token, newToken) loginSuccessfulResponseWithToken(account.username, token, newToken)
context.become(displayingServerListBehavior)
updateServerListTask = updateServerListTask =
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList)
future future
case (_, false) => case (_, false) =>
loginFailureResponseToken(account.username, token, newToken) loginFailureResponseToken(account.username, token, newToken)
Future.successful(None) loginFailureAction()
case (true, _) => case (true, _) =>
loginAccountFailureResponseToken(account.username, token, newToken) loginAccountFailureResponseToken(account.username, token, newToken)
Future.successful(None) loginFailureAction()
} }
case None => Future.successful(None) case None =>
loginFailureAction()
} }
} yield login } yield login
result.onComplete { result.onComplete {
case Success(_) => case Success(_) => ()
case Failure(e) => log.error(e.getMessage) case Failure(e) => log.error(e.getMessage)
} }
} }
def loginSuccessfulResponse(username: String, newToken: String): Unit = { private def loginSuccessfulResponse(username: String, token: String): Unit = {
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.Success, LoginError.Success,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -304,26 +402,21 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginSuccessfulResponseToken(username: String, token: String, newToken: String): Unit = { private def loginSuccessfulResponseWithToken(username: String, token: String, newToken: String): Unit = {
log.info(s"User $username logged in unsing token $token") log.info(s"User $username logged in using token $token")
middlewareActor ! MiddlewareActor.Send( loginSuccessfulResponse(username, newToken)
LoginRespMessage(
newToken,
LoginError.Success,
StationError.AccountActive,
StationSubscriptionStatus.Active,
0,
username,
10001
)
)
} }
def loginPwdFailureResponse(username: String, newToken: String): Unit = { private def loginFailureAction(): Future[Any] = {
context.become(accountLoginBehavior)
Future.successful(None)
}
private def loginFailurePasswordResponse(username: String, token: String): Unit = {
log.warning(s"Failed login to account $username") log.warning(s"Failed login to account $username")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.BadUsernameOrPassword, LoginError.BadUsernameOrPassword,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -334,7 +427,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseToken(token: String, newToken: String): Unit = { private def loginFailureResponseToken(token: String, newToken: String): Unit = {
log.warning(s"Failed login using unknown token $token") log.warning(s"Failed login using unknown token $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -349,7 +442,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { private def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = {
log.warning(s"Failed login using expired token $token") log.warning(s"Failed login using expired token $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -364,11 +457,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponse(username: String, newToken: String): Unit = { private def loginFailureResponse(username: String, token: String): Unit = {
log.warning(s"DB problem username: $username") log.warning(s"DB problem username: $username")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.unk1, LoginError.unk1,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -379,7 +472,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { private def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = {
log.warning(s"DB problem username $username token: $token") log.warning(s"DB problem username $username token: $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -394,11 +487,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginAccountFailureResponse(username: String, newToken: String): Unit = { private def loginAccountFailureResponse(username: String, token: String): Unit = {
log.warning(s"Account $username inactive") log.warning(s"Account $username inactive")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.BadUsernameOrPassword, LoginError.BadUsernameOrPassword,
StationError.AccountClosed, StationError.AccountClosed,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -409,7 +502,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { private def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = {
log.warning(s"Account $username inactive token: $token ") log.warning(s"Account $username inactive token: $token ")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -424,16 +517,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def generateToken(): String = { private def updateServerList(): Unit = {
val r = new scala.util.Random //todo list of game servers from database, eventually, which is a separation of game server from login server
val sb = new mutable.StringBuilder
for (_ <- 1 to 31) {
sb.append(r.nextPrintableChar())
}
sb.toString
}
def updateServerList(): Unit = {
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
VNLWorldStatusMessage( VNLWorldStatusMessage(
"Welcome to PlanetSide! ", "Welcome to PlanetSide! ",
@ -450,7 +535,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def failWithError(error: String): Unit = { private def failWithError(error: String): Unit = {
log.error(error) log.error(error)
middlewareActor ! MiddlewareActor.Close() middlewareActor ! MiddlewareActor.Close()
} }