mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-03-03 12:10:22 +00:00
Account and Character Database and Config Improvements (#317)
* Create Account/DB abstraction * Fix crash when removing boomers from deconstructed player * Extend config to include database and worldserver info * Improve ConfigParser tests * Add database setup documentation * Add xTriad to THANKS file ** * Increase bcrypt rounds and fix readme link
This commit is contained in:
parent
ae768e1e42
commit
d08911d07c
29 changed files with 1757 additions and 225 deletions
|
|
@ -3,14 +3,18 @@ package net.psforever.config
|
|||
|
||||
import org.ini4j
|
||||
import scala.reflect.ClassTag
|
||||
import scala.reflect.runtime.universe.TypeTag
|
||||
import scala.annotation.implicitNotFound
|
||||
import scala.concurrent.duration._
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
case class ConfigValueMapper[T](name: String)(f: (String => Option[T])) {
|
||||
def apply(t: String): Option[T] = f(t)
|
||||
}
|
||||
|
||||
object ConfigValueMapper {
|
||||
import scala.language.implicitConversions
|
||||
|
||||
implicit val toInt : ConfigValueMapper[Int] = ConfigValueMapper[Int]("toInt") { e =>
|
||||
try {
|
||||
Some(e.toInt)
|
||||
|
|
@ -19,6 +23,17 @@ object ConfigValueMapper {
|
|||
}
|
||||
}
|
||||
|
||||
// TypeTag is necessary to be able to retrieve an instance of the Enum class
|
||||
// at runtime as it is usually erased at runtime. This is low cost as its only
|
||||
// used during the config parsing
|
||||
implicit def toEnum[E <: Enumeration#Value](implicit m: TypeTag[E]) : ConfigValueMapper[E] = ConfigValueMapper[E]("toEnum") { e =>
|
||||
try {
|
||||
Some(EnumReflector.withName[E](e))
|
||||
} catch {
|
||||
case e: Exception => None
|
||||
}
|
||||
}
|
||||
|
||||
implicit val toBool : ConfigValueMapper[Boolean] = ConfigValueMapper[Boolean]("toBool") { e =>
|
||||
if (e == "yes") {
|
||||
Some(true)
|
||||
|
|
@ -77,6 +92,13 @@ final case class ConfigEntryBool(key: String, default : Boolean, constraints : C
|
|||
def read(v : String) = ConfigValueMapper.toBool(v)
|
||||
}
|
||||
|
||||
final case class ConfigEntryEnum[E <: Enumeration](key: String, default : E#Value)(implicit m : TypeTag[E#Value], implicit val m2 : TypeTag[E#ValueSet]) extends ConfigEntry {
|
||||
type Value = E#Value
|
||||
val constraints : Seq[Constraint[E#Value]] = Seq()
|
||||
def getType = EnumReflector.values[E#ValueSet](m2).mkString(", ")
|
||||
def read(v : String) = ConfigValueMapper.toEnum[E#Value](m)(v)
|
||||
}
|
||||
|
||||
final case class ConfigEntryFloat(key: String, default : Float, constraints : Constraint[Float]*) extends ConfigEntry {
|
||||
type Value = Float
|
||||
def getType = "Float"
|
||||
|
|
@ -101,7 +123,7 @@ object ConfigTypeRequired {
|
|||
}
|
||||
|
||||
trait ConfigParser {
|
||||
protected var config_map : Map[String, Any]
|
||||
protected var config_map : Map[String, Any] = Map()
|
||||
protected val config_template : Seq[ConfigSection]
|
||||
|
||||
// Misuse of this function can lead to run time exceptions when the types don't match
|
||||
|
|
@ -118,8 +140,25 @@ trait ConfigParser {
|
|||
}
|
||||
|
||||
def Load(filename : String) : ValidationResult = {
|
||||
var map : Map[String, Any] = scala.collection.mutable.Map()
|
||||
|
||||
LoadMap(filename, map) match {
|
||||
case i : Invalid =>
|
||||
i
|
||||
case Valid =>
|
||||
ReplaceConfig(map)
|
||||
// run post-parse validation only if we successfully parsed
|
||||
postParseChecks match {
|
||||
case i : Invalid =>
|
||||
i
|
||||
case Valid =>
|
||||
Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def LoadMap(filename : String, map : Map[String, Any]) : ValidationResult = {
|
||||
val ini = new org.ini4j.Ini()
|
||||
config_map = Map()
|
||||
|
||||
try {
|
||||
ini.load(new java.io.File(filename))
|
||||
|
|
@ -136,7 +175,7 @@ trait ConfigParser {
|
|||
if (sectionIni == null)
|
||||
Seq(Invalid("section.missing", section.name))
|
||||
else
|
||||
section.entries.map(parseSection(sectionIni, _))
|
||||
section.entries.map(parseSection(sectionIni, _, map))
|
||||
}.reduceLeft((x, y) => x ++ y)
|
||||
|
||||
val errors : Seq[Invalid] = result.collect { case iv : Invalid => iv }
|
||||
|
|
@ -144,8 +183,11 @@ trait ConfigParser {
|
|||
if (errors.length > 0)
|
||||
errors.reduceLeft((x, y) => x ++ y)
|
||||
else
|
||||
// run post-parse validation only if we successfully parsed
|
||||
postParseChecks
|
||||
Valid
|
||||
}
|
||||
|
||||
def ReplaceConfig(map : Map[String, Any]) {
|
||||
config_map = map
|
||||
}
|
||||
|
||||
def FormatErrors(invalidResult : Invalid) : Seq[String] = {
|
||||
|
|
@ -166,12 +208,12 @@ trait ConfigParser {
|
|||
Valid
|
||||
}
|
||||
|
||||
protected def parseSection(sectionIni : org.ini4j.Profile.Section, entry : ConfigEntry) : ValidationResult = {
|
||||
protected def parseSection(sectionIni : org.ini4j.Profile.Section, entry : ConfigEntry, map : Map[String, Any]) : ValidationResult = {
|
||||
var rawValue = sectionIni.get(entry.key)
|
||||
val full_key = sectionIni.getName + "." + entry.key
|
||||
val full_key : String = sectionIni.getName + "." + entry.key
|
||||
|
||||
val value = if (rawValue == null) {
|
||||
// warn about defaults from unset parameters?
|
||||
println(s"config warning: missing key '${entry.key}', using default value '${entry.default}'")
|
||||
entry.default
|
||||
} else {
|
||||
rawValue = rawValue.stripPrefix("\"").stripSuffix("\"")
|
||||
|
|
@ -181,7 +223,8 @@ trait ConfigParser {
|
|||
case None => return Invalid(ValidationError(String.format("%s: value format error (expected: %s)", full_key, entry.getType)))
|
||||
}
|
||||
}
|
||||
config_map += (full_key -> value)
|
||||
|
||||
map += (full_key -> value)
|
||||
|
||||
ParameterValidator(entry.constraints, Some(value)) match {
|
||||
case v @ Valid => v
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ trait Constraints {
|
|||
require(error != null, "error must not be null")
|
||||
|
||||
if (o == null) Invalid(ValidationError(error, regex))
|
||||
else regex.unapplySeq(o).map(_ => Valid).getOrElse(Invalid(ValidationError(error, regex)))
|
||||
else regex.unapplySeq(o).map(_ => Valid).getOrElse(Invalid(ValidationError(error, name)))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
package net.psforever.config
|
||||
|
||||
import scala.reflect.runtime.universe._
|
||||
|
||||
/**
|
||||
* Scala [[Enumeration]] helpers implementing Scala versions of
|
||||
* Java's [[java.lang.Enum.valueOf(Class[Enum], String)]].
|
||||
* @author Dmitriy Yefremov (http://yefremov.net/blog/scala-enum-by-name/)
|
||||
*/
|
||||
object EnumReflector {
|
||||
|
||||
private val mirror: Mirror = runtimeMirror(getClass.getClassLoader)
|
||||
|
||||
/**
|
||||
* Returns a value of the specified enumeration with the given name.
|
||||
* @param name value name
|
||||
* @tparam T enumeration type
|
||||
* @return enumeration value, see [[scala.Enumeration.withName(String)]]
|
||||
*/
|
||||
def withName[T <: Enumeration#Value: TypeTag](name: String): T = {
|
||||
typeOf[T] match {
|
||||
case valueType @ TypeRef(enumType, _, _) =>
|
||||
val methodSymbol = factoryMethodSymbol(enumType, "withName")
|
||||
val moduleSymbol = enumType.termSymbol.asModule
|
||||
reflect(moduleSymbol, methodSymbol)(name).asInstanceOf[T]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of values of an enumeration
|
||||
* @tparam T enumeration type
|
||||
* @return possible enumeration values, see [[scala.Enumeration.values()]]
|
||||
*/
|
||||
def values[T <: Enumeration#ValueSet: TypeTag]: T = {
|
||||
typeOf[T] match {
|
||||
case valueType @ TypeRef(enumType, _, _) =>
|
||||
val methodSymbol = factoryMethodSymbol(enumType, "values")
|
||||
val moduleSymbol = enumType.termSymbol.asModule
|
||||
reflect(moduleSymbol, methodSymbol)().asInstanceOf[T]
|
||||
}
|
||||
}
|
||||
|
||||
private def factoryMethodSymbol(enumType: Type, name : String): MethodSymbol = {
|
||||
enumType.member(TermName(name)).asMethod
|
||||
}
|
||||
|
||||
private def reflect(module: ModuleSymbol, method: MethodSymbol)(args: Any*): Any = {
|
||||
val moduleMirror = mirror.reflectModule(module)
|
||||
val instanceMirror = mirror.reflect(moduleMirror.instance)
|
||||
instanceMirror.reflectMethod(method)(args:_*)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
class Account(private val accountId : Int, private val username : String, private val gm : Boolean = false) {
|
||||
def AccountId : Int = accountId
|
||||
def Username : String = username
|
||||
def GM : Boolean = gm
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ object ServerType extends Enumeration(1) {
|
|||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||
}
|
||||
|
||||
// This MUST be an IP address. The client DOES NOT do name resolution properly
|
||||
final case class WorldConnectionInfo(address : InetSocketAddress)
|
||||
|
||||
final case class WorldInformation(name : String, status : WorldStatus.Value,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
import scala.collection.mutable
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.Account
|
||||
|
||||
/**
|
||||
* This actor is essentially a bridge between the LoginSessionActor and WorldSessionActor. When
|
||||
* a player connects, the LoginSessionActor handles the account login by querying the database,
|
||||
* comparing passwords etc, and then by inserting the account information into this actor by a
|
||||
* token that is also sent to the player's client in the LoginRespMessage packet. The token is
|
||||
* just like a browser cookie session token.
|
||||
*
|
||||
* When the player's client has processed the LoginRespMessage packet, it attempts to connect
|
||||
* to the game world by sending a ConnectToWorldRequestMessage with the token which is used
|
||||
* by the WorldSessionActor to lookup the player's account info from this actor.
|
||||
*
|
||||
* So this actor is a temporary store of account data for players logging into the login
|
||||
* server and crossing over to the world server.
|
||||
*/
|
||||
|
||||
class AccountIntermediaryService extends Actor {
|
||||
private val accountsByToken = mutable.Map[String, Account]()
|
||||
private val IPAddressBySessionID = mutable.Map[Long, IPAddress]()
|
||||
private [this] val log = org.log4s.getLogger
|
||||
|
||||
override def preStart = {
|
||||
log.trace("Starting...")
|
||||
}
|
||||
|
||||
def receive = {
|
||||
// Called by the LoginSessionActor
|
||||
case StoreAccountData(token, account) =>
|
||||
accountsByToken += (token -> account)
|
||||
log.info(s"Storing intermediary account data for ${account.AccountId}")
|
||||
|
||||
// Called by the WorldSessionActor
|
||||
case RetrieveAccountData(token) =>
|
||||
val account : Option[Account] = accountsByToken.remove(token)
|
||||
if(account.nonEmpty) {
|
||||
sender() ! ReceiveAccountData(account.get)
|
||||
log.info(s"Retrieving intermediary account data for ${account.get.AccountId}")
|
||||
} else {
|
||||
log.error(s"Unable to retrieve intermediary account data for ${account.get.AccountId}")
|
||||
}
|
||||
|
||||
case StoreIPAddress(sessionID, address) =>
|
||||
IPAddressBySessionID += (sessionID -> address)
|
||||
log.info(s"Storing IP address (${address.Address}) for sessionID : $sessionID")
|
||||
|
||||
case RetrieveIPAddress(sessionID) =>
|
||||
val address : Option[IPAddress] = IPAddressBySessionID.remove(sessionID)
|
||||
if(address.nonEmpty) {
|
||||
sender() ! ReceiveIPAddress(address.get)
|
||||
log.info(s"Retrieving IP address data for sessionID : ${sessionID}")
|
||||
} else {
|
||||
log.error(s"Unable to retrieve IP address data for sessionID : ${sessionID}")
|
||||
}
|
||||
|
||||
case msg =>
|
||||
log.warn(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
}
|
||||
11
common/src/main/scala/services/account/IPAddress.scala
Normal file
11
common/src/main/scala/services/account/IPAddress.scala
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
class IPAddress(private val address: InetSocketAddress) {
|
||||
def Address : String = address.getAddress.getHostAddress
|
||||
def CanonicalHostName : String = address.getAddress.getCanonicalHostName
|
||||
def HostName : String = address.getAddress.getHostName
|
||||
def Port : Int = address.getPort
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
import net.psforever.objects.Account
|
||||
|
||||
final case class ReceiveAccountData(account: Account)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
final case class ReceiveIPAddress(address : IPAddress)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
final case class RetrieveAccountData(token : String)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
final case class RetrieveIPAddress(sessionID : Long)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
import net.psforever.objects.Account
|
||||
|
||||
final case class StoreAccountData(token : String, account : Account)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.account
|
||||
|
||||
final case class StoreIPAddress(sessionID : Long, address : IPAddress)
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue