mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Merge pull request #103 from psforever/develop/chord
Improve LoginRespMessage, write test, and begin DB
This commit is contained in:
commit
0f50c8b15f
|
|
@ -16,7 +16,8 @@ lazy val commonSettings = Seq(
|
|||
"org.log4s" %% "log4s" % "1.3.0",
|
||||
"org.fusesource.jansi" % "jansi" % "1.12",
|
||||
"org.scoverage" %% "scalac-scoverage-plugin" % "1.1.1",
|
||||
"com.github.nscala-time" %% "nscala-time" % "2.12.0"
|
||||
"com.github.nscala-time" %% "nscala-time" % "2.12.0",
|
||||
"com.github.mauricio" %% "mysql-async" % "0.2.21"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ final case class LoginMessage(majorVersion : Long,
|
|||
}
|
||||
|
||||
object LoginMessage extends Marshallable[LoginMessage] {
|
||||
lazy val tokenCodec = paddedFixedSizeBytes(32, cstring, ignore(8))
|
||||
|
||||
private def username = PacketHelpers.encodedStringAligned(7)
|
||||
private def password = PacketHelpers.encodedString
|
||||
private def tokenPath = paddedFixedSizeBytes(32, cstring, ignore(8)) :: username
|
||||
private def tokenPath = tokenCodec :: username
|
||||
private def passwordPath = username :: password
|
||||
|
||||
type Struct = String :: Option[String] :: Option[String] :: HNil
|
||||
|
|
|
|||
|
|
@ -3,32 +3,81 @@ package net.psforever.packet.game
|
|||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import scodec.Codec
|
||||
import scodec.bits.ByteVector
|
||||
import scodec.codecs._
|
||||
|
||||
final case class LoginRespMessage(token : String, // printable ascii for 16
|
||||
unknown : ByteVector, // hex"00000000 18FABE0C 00000000 00000000"
|
||||
error : Long, // 0
|
||||
stationError : Long, // 1
|
||||
subscriptionStatus : Long, // 2 or 5
|
||||
someToken : Long, // 685276011
|
||||
username : String, // the user
|
||||
/**
|
||||
* This message is sent from the server to the client upon reception of a [[LoginMessage]].
|
||||
*
|
||||
* The result of the login is contained in this message. When a login is successful, a session token
|
||||
* is returned to the client which then forwards this to the World server it chooses to connect to.
|
||||
*
|
||||
* In terms of failed logins, the PS client favors errors in this order
|
||||
*
|
||||
* 1. LoginError
|
||||
* 2. StationError
|
||||
* 3. StationSubscriptionStatus
|
||||
*
|
||||
* Don't try and set more than one error at the same time. Just provide a single error message to be displayed.
|
||||
*
|
||||
* @param token A 'token' which acts exactly like a session cookie in a browser. Allows logins to not use a password
|
||||
* @param error A general login error message
|
||||
* @param stationError A PlanetSide Sony Online Entertainment (SOE) station result
|
||||
* @param subscriptionStatus A response detailing the current subscription type
|
||||
* @param unkUIRelated An unknown possible bitfield that controls some game variables (possibly expansions?)
|
||||
* @param username The login username
|
||||
* @param privilege If set above 10000, then the user has access to GM commands. Not sure of other values.
|
||||
*/
|
||||
final case class LoginRespMessage(token : String,
|
||||
error : LoginRespMessage.LoginError.Type,
|
||||
stationError : LoginRespMessage.StationError.Type,
|
||||
subscriptionStatus : LoginRespMessage.StationSubscriptionStatus.Type,
|
||||
unkUIRelated : Long,
|
||||
username : String,
|
||||
privilege : Long) extends PlanetSideGamePacket {
|
||||
def opcode = GamePacketOpcode.LoginRespMessage
|
||||
def encode = LoginRespMessage.encode(this)
|
||||
}
|
||||
|
||||
|
||||
object LoginRespMessage extends Marshallable[LoginRespMessage] {
|
||||
|
||||
object LoginError extends Enumeration {
|
||||
type Type = Value
|
||||
val Success = Value(0)
|
||||
val BadUsernameOrPassword = Value(5)
|
||||
val BadVersion = Value(0xf)
|
||||
|
||||
implicit val codec = PacketHelpers.createLongEnumerationCodec(this, uint32L)
|
||||
}
|
||||
|
||||
object StationError extends Enumeration {
|
||||
type Type = Value
|
||||
val AccountActive = Value(1)
|
||||
val AccountClosed = Value(2)
|
||||
|
||||
implicit val codec = PacketHelpers.createLongEnumerationCodec(this, uint32L)
|
||||
}
|
||||
|
||||
object StationSubscriptionStatus extends Enumeration {
|
||||
type Type = Value
|
||||
val None = Value(1)
|
||||
val Active = Value(2) /// Not sure about this one (guessing)
|
||||
val Closed = Value(4)
|
||||
val Trial = Value(5) /// Not sure about this one either
|
||||
val TrialExpired = Value(6)
|
||||
|
||||
implicit val codec = PacketHelpers.createLongEnumerationCodec(this, uint32L)
|
||||
}
|
||||
|
||||
implicit val codec : Codec[LoginRespMessage] = (
|
||||
("token" | fixedSizeBytes(16, ascii)) ::
|
||||
("unknown" | bytes(16)) ::
|
||||
("error" | uint32L) ::
|
||||
("station_error" | uint32L) ::
|
||||
("subscription_status" | uint32L) ::
|
||||
("token" | LoginMessage.tokenCodec) ::
|
||||
("error" | LoginError.codec) ::
|
||||
("station_error" | StationError.codec) ::
|
||||
("subscription_status" | StationSubscriptionStatus.codec) ::
|
||||
("unknown" | uint32L) ::
|
||||
("username" | PacketHelpers.encodedString) ::
|
||||
("privilege" | uint32L)
|
||||
.flatZip(priv => bool)
|
||||
.flatZip(priv => bool) // really not so sure about this bool part. client gets just a single bit
|
||||
.xmap[Long]({case (a, _) => a}, priv => (priv, (priv & 1) == 1))
|
||||
).as[LoginRespMessage]
|
||||
}
|
||||
67
common/src/test/scala/game/LoginRespMessageTest.scala
Normal file
67
common/src/test/scala/game/LoginRespMessageTest.scala
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) 2017 PSForever.net to present
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.LoginRespMessage._
|
||||
import scodec.bits._
|
||||
|
||||
class LoginRespMessageTest extends Specification {
|
||||
|
||||
/// NOTE: the token is a C-string, meaning that the 18FABE0C junk seems like uninitialized memory due to memcpy, but
|
||||
/// not memset
|
||||
//val original = hex"02 4861484C64597A73305641536A6B73520000000018FABE0C0000000000000000" ++
|
||||
// hex"00000000 01000000 02000000 6B7BD828 8C4169666671756F7469656E74 00000000 00"
|
||||
val string = hex"02 4861484C64597A73305641536A6B735200000000000000000000000000000000" ++
|
||||
hex"00000000 01000000 02000000 6B7BD828 8C4169666671756F7469656E74 00000000 00"
|
||||
|
||||
val string_priv = hex"02 4861484C64597A73305641536A6B735200000000000000000000000000000000" ++
|
||||
hex"00000000 01000000 02000000 6B7BD828 8C4169666671756F7469656E74 11270000 80"
|
||||
|
||||
"encode" in {
|
||||
val msg = LoginRespMessage("HaHLdYzs0VASjksR", LoginError.Success, StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active, 685276011, "Aiffquotient", 0)
|
||||
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
||||
"encode with privilege" in {
|
||||
val msg = LoginRespMessage("HaHLdYzs0VASjksR", LoginError.Success, StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active, 685276011, "Aiffquotient", 10001)
|
||||
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
pkt mustEqual string_priv
|
||||
}
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case LoginRespMessage(token, error, stationError, subscription, unk, username, priv) =>
|
||||
token mustEqual "HaHLdYzs0VASjksR"
|
||||
error mustEqual LoginError.Success
|
||||
stationError mustEqual StationError.AccountActive
|
||||
subscription mustEqual StationSubscriptionStatus.Active
|
||||
unk mustEqual 685276011
|
||||
username mustEqual "Aiffquotient"
|
||||
priv mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode with privilege" in {
|
||||
PacketCoding.DecodePacket(string_priv).require match {
|
||||
case LoginRespMessage(token, error, stationError, subscription, unk, username, priv) =>
|
||||
token mustEqual "HaHLdYzs0VASjksR"
|
||||
error mustEqual LoginError.Success
|
||||
stationError mustEqual StationError.AccountActive
|
||||
subscription mustEqual StationSubscriptionStatus.Active
|
||||
unk mustEqual 685276011
|
||||
username mustEqual "Aiffquotient"
|
||||
priv mustEqual 10001
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
}
|
||||
9
pslogin/src/main/scala/DatabaseConnector.scala
Normal file
9
pslogin/src/main/scala/DatabaseConnector.scala
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) 2017 PSForever.net to present
|
||||
import com.github.mauricio.async.db.Connection
|
||||
import com.github.mauricio.async.db.mysql.MySQLConnection
|
||||
import com.github.mauricio.async.db.mysql.util.URLParser
|
||||
|
||||
object DatabaseConnector {
|
||||
val accounts_db = URLParser.parse("jdbc:mysql://localhost:3306/psforever-accounts?user=root&password=PSForever")
|
||||
def getAccountsConnection = new MySQLConnection(accounts_db)
|
||||
}
|
||||
|
|
@ -9,8 +9,13 @@ import org.log4s.MDC
|
|||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
import MDCContextAware.Implicits._
|
||||
import com.github.mauricio.async.db.{Connection, QueryResult, RowData}
|
||||
import com.github.mauricio.async.db.mysql.MySQLConnection
|
||||
import com.github.mauricio.async.db.mysql.exceptions.MySQLException
|
||||
import com.github.mauricio.async.db.mysql.util.URLParser
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
|
|
@ -74,14 +79,17 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
def handleControlPkt(pkt : PlanetSideControlPacket) = {
|
||||
pkt match {
|
||||
case SlottedMetaPacket(slot, subslot, innerPacket) =>
|
||||
// Meta packets are like TCP packets - then need to be ACKed to the client
|
||||
sendResponse(PacketCoding.CreateControlPacket(SlottedMetaAck(slot, subslot)))
|
||||
|
||||
PacketCoding.DecodePacket(innerPacket) match {
|
||||
case Failure(e) =>
|
||||
log.error(s"Failed to decode inner packet of SlottedMetaPacket: $e")
|
||||
case Successful(v) =>
|
||||
handlePkt(v)
|
||||
}
|
||||
// Decode the inner packet and handle it or error
|
||||
PacketCoding.DecodePacket(innerPacket).fold({
|
||||
error => log.error(s"Failed to decode inner packet of SlottedMetaPacket: $error")
|
||||
}, {
|
||||
handlePkt(_)
|
||||
})
|
||||
/// TODO: figure out what this is what what it does for the PS client
|
||||
/// I believe it has something to do with reliable packet transmission and resending
|
||||
case sync @ ControlSync(diff, unk, f1, f2, f3, f4, fa, fb) =>
|
||||
log.trace(s"SYNC: ${sync}")
|
||||
|
||||
|
|
@ -89,16 +97,17 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick,
|
||||
fa, fb, fb, fa)))
|
||||
case MultiPacket(packets) =>
|
||||
|
||||
/// Extract out each of the subpackets in the MultiPacket and handle them or raise a packet error
|
||||
packets.foreach { pkt =>
|
||||
PacketCoding.DecodePacket(pkt) match {
|
||||
case Failure(e) =>
|
||||
log.error(s"Failed to decode inner packet of MultiPacket: $e")
|
||||
case Successful(v) =>
|
||||
handlePkt(v)
|
||||
}
|
||||
PacketCoding.DecodePacket(pkt).fold({ error =>
|
||||
log.error(s"Failed to decode inner packet of MultiPacket: $error")
|
||||
}, {
|
||||
handlePkt(_)
|
||||
})
|
||||
}
|
||||
case default =>
|
||||
log.debug(s"Unhandled ControlPacket $default")
|
||||
log.error(s"Unhandled ControlPacket $default")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,9 +115,51 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
val serverName = "PSForever"
|
||||
val serverAddress = new InetSocketAddress(LoginConfig.serverIpAddress.getHostAddress, 51001)
|
||||
|
||||
// TESTING CODE FOR ACCOUNT LOOKUP
|
||||
def accountLookup(username : String, password : String) : Boolean = {
|
||||
val connection: Connection = DatabaseConnector.getAccountsConnection
|
||||
|
||||
Await.result(connection.connect, 5 seconds)
|
||||
|
||||
// create account
|
||||
// username, password, email
|
||||
// Result: worked or failed
|
||||
// login to account
|
||||
// username, password
|
||||
// Result: token (session cookie)
|
||||
|
||||
val future: Future[QueryResult] = connection.sendPreparedStatement("SELECT * FROM accounts where username=?", Array(username))
|
||||
|
||||
val mapResult: Future[Any] = future.map(queryResult => queryResult.rows match {
|
||||
case Some(resultSet) => {
|
||||
val row : RowData = resultSet.head
|
||||
row(0)
|
||||
}
|
||||
case None => -1
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
// XXX: remove awaits
|
||||
val result = Await.result( mapResult, 5 seconds )
|
||||
return true
|
||||
} catch {
|
||||
case e : MySQLException =>
|
||||
log.error(s"SQL exception $e")
|
||||
case e: Exception =>
|
||||
log.error(s"Unknown exception when executing SQL statement: $e")
|
||||
} finally {
|
||||
connection.disconnect
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
def handleGamePkt(pkt : PlanetSideGamePacket) = 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
|
||||
import game.LoginRespMessage._
|
||||
|
||||
val clientVersion = s"Client Version: ${majorVersion}.${minorVersion}.${revision}, ${buildDate}"
|
||||
|
||||
|
|
@ -117,13 +168,28 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
else
|
||||
log.info(s"New login UN:$username PW:$password. ${clientVersion}")
|
||||
|
||||
val newToken = token.getOrElse("THISISMYTOKENYES")
|
||||
val response = LoginRespMessage(newToken, hex"00000000 18FABE0C 00000000 00000000",
|
||||
0, 1, 2, 685276011, username, 10001)
|
||||
// This is temporary until a schema has been developed
|
||||
//val loginSucceeded = accountLookup(username, password.getOrElse(token.get))
|
||||
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||
// Allow any one to login for now
|
||||
val loginSucceeded = true
|
||||
|
||||
updateServerListTask = context.system.scheduler.schedule(0 seconds, 2 seconds, self, UpdateServerList())
|
||||
if(loginSucceeded) {
|
||||
val newToken = token.getOrElse("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHH")
|
||||
val response = LoginRespMessage(newToken, LoginError.Success, StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active, 0, username, 10001)
|
||||
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||
|
||||
updateServerListTask = context.system.scheduler.schedule(0 seconds, 2 seconds, self, UpdateServerList())
|
||||
} else {
|
||||
val newToken = token.getOrElse("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHH")
|
||||
val response = LoginRespMessage(newToken, LoginError.BadUsernameOrPassword, StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active, 685276011, username, 10001)
|
||||
|
||||
log.info(s"Failed login to account ${username}")
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||
}
|
||||
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
|
||||
log.info(s"Connect to world request for '${name}'")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue