[packet] VNLWorldStatusMessage

Added VNL packet type from IDA. Moved definition in to its own file.
Refactored PacketCoding MarshalPacket. The whole structure needs a
rework.

Now able to get a PlanetSide client to the server screen with a server
of choice.
This commit is contained in:
Chord 2016-03-04 13:00:03 -05:00
parent 1643ecc1dd
commit b8ff34c0f9
10 changed files with 363 additions and 102 deletions

View file

@ -59,10 +59,5 @@ object ControlPacketOpcode extends Enumeration {
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}")) case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}"))
} }
val storageType = uint8L implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
} }

View file

@ -8,14 +8,14 @@ object GamePacketOpcode extends Enumeration {
type Type = Value type Type = Value
val val
// Opcodes should have a marker every 10 // Opcodes should have a marker every 10 (decimal)
// OPCODE 0 // OPCODE 0
Unknown0, Unknown0,
LoginMessage, LoginMessage,
LoginRespMessage, LoginRespMessage,
Unknown3, Unknown3,
ConnectToWorldMessage, ConnectToWorldMessage,
Unknown5, VNLWorldStatusMessage,
UnknownMessage6, UnknownMessage6,
UnknownMessage7, UnknownMessage7,
PlayerStateMessage, PlayerStateMessage,
@ -33,13 +33,9 @@ object GamePacketOpcode extends Enumeration {
def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match { def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match {
case LoginMessage => psforever.net.LoginMessage.decode case LoginMessage => psforever.net.LoginMessage.decode
case LoginRespMessage => psforever.net.LoginRespMessage.decode case LoginRespMessage => psforever.net.LoginRespMessage.decode
case VNLWorldStatusMessage => psforever.net.VNLWorldStatusMessage.decode
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}")) case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}"))
} }
val storageType = uint8L implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
} }

View file

@ -1,5 +1,7 @@
package psforever.net package psforever.net
import java.nio.charset.Charset
import scodec.{DecodeResult, Err, Codec, Attempt} import scodec.{DecodeResult, Err, Codec, Attempt}
import scodec.bits._ import scodec.bits._
import scodec.codecs._ import scodec.codecs._
@ -14,22 +16,22 @@ sealed trait PlanetSidePacket extends Serializable {
} }
// Used by companion objects to create encoders and decoders // Used by companion objects to create encoders and decoders
sealed trait Marshallable[T] { trait Marshallable[T] {
implicit val codec : Codec[T] implicit val codec : Codec[T]
def encode(a : T) : Attempt[BitVector] = codec.encode(a) def encode(a : T) : Attempt[BitVector] = codec.encode(a)
// assert that when decoding a marshallable type, that no bits are left over // assert that when decoding a marshallable type, that no bits are left over
def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.complete.decode(a) def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.decode(a)
} }
sealed trait PlanetSideGamePacket extends PlanetSidePacket { trait PlanetSideGamePacket extends PlanetSidePacket {
def opcode : GamePacketOpcode.Type def opcode : GamePacketOpcode.Type
} }
sealed trait PlanetSideControlPacket extends PlanetSidePacket { trait PlanetSideControlPacket extends PlanetSidePacket {
def opcode : ControlPacketOpcode.Type def opcode : ControlPacketOpcode.Type
} }
sealed trait PlanetSideCryptoPacket extends PlanetSidePacket { trait PlanetSideCryptoPacket extends PlanetSidePacket {
def opcode : CryptoPacketOpcode.Type def opcode : CryptoPacketOpcode.Type
} }
@ -143,6 +145,17 @@ object LoginMessage extends Marshallable[LoginMessage] {
type Struct = String :: Option[String] :: Option[String] :: HNil type Struct = String :: Option[String] :: Option[String] :: HNil
/* Okay, okay, here's what's happening here:
PlanetSide's *wonderful* packet design reuses packets for different encodings.
What we have here is that depending on a boolean in the LoginPacket, we will either
be decoding a username & password OR a token & username. Yeah...so this doesn't
really fit in to a fixed packet decoding scheme.
The below code abstracts away from this by using pattern matching.
The scodec specific part is the either(...) Codec, which decodes one bit and chooses
Left or Right depending on it.
*/
implicit val credentialChoice : Codec[Struct] = { implicit val credentialChoice : Codec[Struct] = {
type InStruct = Either[String :: String :: HNil, String :: String :: HNil] type InStruct = Either[String :: String :: HNil, String :: String :: HNil]
@ -307,12 +320,7 @@ object PacketType extends Enumeration(1) {
type Type = Value type Type = Value
val ResetSequence, Unknown2, Crypto, Normal = Value val ResetSequence, Unknown2, Crypto, Normal = Value
val storageType = uint4L implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint4L)
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
} }
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] { object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
@ -327,10 +335,16 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// TODO: figure out why I can't insert codecs without using a new case class /*class MarshallableEnum[+T] extends Enumeration {
// Notes: https://mpilquist.github.io/blog/2013/06/09/scodec-part-3/ type StorageType = Codec[Int]
// https://stackoverflow.com/questions/29585649/using-nested-case-classes-with-scodec
// https://gist.github.com/travisbrown/3945529 implicit val storageType : StorageType = uint8
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
implicit val codec: Codec[T] = PacketHelpers.createEnumerationCodec(this, storageType)
}*/
object PacketHelpers { object PacketHelpers {
def emptyCodec[T](instance : T) = { def emptyCodec[T](instance : T) = {
@ -339,13 +353,14 @@ object PacketHelpers {
Codec[HNil].xmap[T](from, to) Codec[HNil].xmap[T](from, to)
} }
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = { def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
type Struct = Int :: HNil type Struct = Int :: HNil
val struct: Codec[Struct] = storageCodec.hlist val struct: Codec[Struct] = storageCodec.hlist
// Assure that the enum will always be able to fit in a N-bit int // Assure that the enum will always be able to fit in a N-bit int
assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2), assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type") enum.getClass.getCanonicalName + ": maxId exceeds primitive type")
def to(pkt: E#Value): Struct = { def to(pkt: E#Value): Struct = {
pkt.id :: HNil pkt.id :: HNil
@ -372,15 +387,47 @@ object PacketHelpers {
private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)). private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)).
xmap[Int]( xmap[Int](
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a), (a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
(a : Int) => if(a > 0x7f) Left(a) else Right(a) (a : Int) =>
// if the specified goes above 0x7f (127) then we need two bytes to represent it
if(a > 0x7f) Left(a) else Right(a)
) )
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = either(bool, uint(15), uint(7)). /*private def encodedStringSizeWithLimit(limit : Int) : Codec[Int] = {
xmap[Int]( either(bool, uint(15), uint(7)).
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a), exmap[Int](
(a : Int) => if(a > 0x7f) Left(a) else Right(a) (a : Either[Int, Int]) => {
) <~ ignore(pad) val result = a.fold[Int](a => a, a => a)
if(result > limit)
Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))
else
Attempt.successful(result)
},
(a : Int) => {
if(a > limit)
return Attempt.failure(Err("adsf"))
//return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")))
if(a > 0x7f)
return Attempt.successful(Left(a))
else
Right(a)
}
)
}*/
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = encodedStringSize <~ ignore(pad)
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii) def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
//def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii)
def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii) def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii)
/// Variable for the charset that planetside uses for unicode
val utf16 = string(Charset.forName("UTF-16LE"))
/// An encoded *wide* string is twice the length of the given encoded size and half of the length of the
/// input string. We use xmap to transform the encodedString codec as this change is just a division and multiply
def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
insize => insize*2,
outSize => outSize/2), utf16)
} }

View file

@ -7,32 +7,41 @@ import scodec.bits._
import scodec.{DecodeResult, Err, Attempt, Codec} import scodec.{DecodeResult, Err, Attempt, Codec}
import scodec.codecs.{uint16L, uint8L, uint4L, bytes} import scodec.codecs.{uint16L, uint8L, uint4L, bytes}
// Packet containers /// Packet container base trait
sealed trait PlanetSidePacketContainer sealed trait PlanetSidePacketContainer
// A sequence, encrypted opcode, encrypted payload, and MD5MAC plus padding /// A sequence, encrypted opcode, encrypted payload, and implicit MD5MAC plus padding
final case class EncryptedPacket(sequenceNumber : Int, final case class EncryptedPacket(sequenceNumber : Int,
payload : ByteVector) extends PlanetSidePacketContainer payload : ByteVector) extends PlanetSidePacketContainer
// A sequence, and payload. Crypto packets have no discernible opcodes /// A sequence, and payload. Crypto packets have no discernible opcodes an rely off of implicit
/// state to decode properly
final case class CryptoPacket(sequenceNumber : Int, final case class CryptoPacket(sequenceNumber : Int,
packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer
/// A sequenced game packet with an opcode and payload
final case class GamePacket(opcode : GamePacketOpcode.Value, final case class GamePacket(opcode : GamePacketOpcode.Value,
sequenceNumber : Int, sequenceNumber : Int,
packet : PlanetSideGamePacket) extends PlanetSidePacketContainer packet : PlanetSideGamePacket) extends PlanetSidePacketContainer
// Just an opcode + payload (does not expect a response) /// Just an opcode + payload
final case class ControlPacket(opcode : ControlPacketOpcode.Value, final case class ControlPacket(opcode : ControlPacketOpcode.Value,
packet : PlanetSideControlPacket) extends PlanetSidePacketContainer packet : PlanetSideControlPacket) extends PlanetSidePacketContainer
object PacketCoding { object PacketCoding {
/// A lower bound on the packet size
final val PLANETSIDE_MIN_PACKET_SIZE = 2 final val PLANETSIDE_MIN_PACKET_SIZE = 2
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = { /**
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore) * Given a full and complete planetside packet as it would be sent on the wire, attempt to
} * decode it given an optional header and required payload. This function does all of the
* hard work of making decisions along the way in order to decode a planetside packet to
* completion.
* @param msg the raw packet
* @param cryptoState the current state of the connection's crypto. This is only used when decoding
* crypto packets as they do not have opcodes
* @return PlanetSidePacketContainer
*/
def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
// check for a minimum length // check for a minimum length
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
@ -47,6 +56,22 @@ object PacketCoding {
} }
} }
/**
* Helper function to decode a packet without specifying a crypto packet state.
* Mostly used when there is no crypto state available, such as tests.
* @param msg packet data bytes
* @return PlanetSidePacketContainer
*/
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
}
/**
* Similar to UnmarshalPacket, but does not process any packet header and does not support
* decoding of crypto packets. Mostly used in tests.
* @param msg raw, unencrypted packet
* @return PlanetSidePacket
*/
def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = { def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = {
// check for a minimum length // check for a minimum length
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
@ -68,70 +93,110 @@ object PacketCoding {
var opcodeEncoded : BitVector = BitVector.empty var opcodeEncoded : BitVector = BitVector.empty
var payloadEncoded : BitVector = BitVector.empty var payloadEncoded : BitVector = BitVector.empty
var controlPacket = false
var sequenceNum = 0
// packet flags
var hasFlags = true
var secured = false
var packetType = PacketType.Crypto
packet match { packet match {
case GamePacket(opcode, seq, payload) => case GamePacket(opcode, seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false) secured = false
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require packetType = PacketType.Normal
sequenceNum = seq
GamePacketOpcode.codec.encode(opcode) match { EncodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) case f @ Failure(e) => return f
case Successful(p) => opcodeEncoded = p
}
uint16L.encode(seq) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $opcode: " + e.messageWithContext))
case Successful(p) => seqEncoded = p
}
encodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p case Successful(p) => payloadEncoded = p
} }
case ControlPacket(opcode, payload) => case ControlPacket(opcode, payload) =>
flagsEncoded = hex"00".bits controlPacket = true
ControlPacketOpcode.codec.encode(opcode) match { EncodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) case f @ Failure(e) => return f
case Successful(p) => opcodeEncoded = p
}
encodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p case Successful(p) => payloadEncoded = p
} }
case CryptoPacket(seq, payload) => case CryptoPacket(seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false) secured = false
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require packetType = PacketType.Crypto
sequenceNum = seq
uint16L.encode(seq) match { EncodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext)) case f @ Failure(e) => return f
case Successful(p) => seqEncoded = p
}
encodePacket(payload) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $payload: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p case Successful(p) => payloadEncoded = p
} }
case EncryptedPacket(seq, payload) => case EncryptedPacket(seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true) secured = true
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require packetType = PacketType.Normal
sequenceNum = seq
// encrypted packets need to be aligned to 4 bytes before encryption/decryption // encrypted packets need to be aligned to 4 bytes before encryption/decryption
// first byte are flags, second and third the sequence, and fourth is the pad // first byte are flags, second and third the sequence, and fourth is the pad
paddingEncoded = hex"00".bits paddingEncoded = hex"00".bits
uint16L.encode(seq) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext))
case Successful(p) => seqEncoded = p
}
payloadEncoded = payload.bits payloadEncoded = payload.bits
} }
val flags = PlanetSidePacketFlags(packetType, secured = secured)
// crypto packets DONT have flags
if(!controlPacket) {
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
uint16L.encode(sequenceNum) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $packet: " + e.messageWithContext))
case Successful(p) => seqEncoded = p
}
}
val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded
Attempt.successful(finalPacket) Attempt.successful(finalPacket)
} }
def EncodePacket(packet : PlanetSideControlPacket) : Attempt[BitVector] = {
val opcode = packet.opcode
var opcodeEncoded = BitVector.empty
var payloadEncoded = BitVector.empty
ControlPacketOpcode.codec.encode(opcode) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in control packet $opcode: " + e.messageWithContext))
case Successful(p) => opcodeEncoded = p
}
encodePacket(packet) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p
}
Attempt.Successful(hex"00".bits ++ opcodeEncoded ++ payloadEncoded)
}
def EncodePacket(packet : PlanetSideCryptoPacket) : Attempt[BitVector] = {
encodePacket(packet) match {
case Failure(e) => Attempt.failure(Err(s"Failed to marshal crypto packet $packet: " + e.messageWithContext))
case s @ Successful(p) => s
}
}
def EncodePacket(packet : PlanetSideGamePacket) : Attempt[BitVector] = {
val opcode = packet.opcode
var opcodeEncoded = BitVector.empty
var payloadEncoded = BitVector.empty
GamePacketOpcode.codec.encode(opcode) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in game packet $opcode: " + e.messageWithContext))
case Successful(p) => opcodeEncoded = p
}
encodePacket(packet) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p
}
Attempt.Successful(opcodeEncoded ++ payloadEncoded)
}
def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet) def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet)
def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet) def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet)
def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet) def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet)
@ -199,6 +264,7 @@ object PacketCoding {
val packet = DecodeControlPacket(msg) val packet = DecodeControlPacket(msg)
packet match { packet match {
// just return the failure
case f @ Failure(e) => f case f @ Failure(e) => f
case Successful(p) => case Successful(p) =>
Attempt.successful(CreateControlPacket(p)) Attempt.successful(CreateControlPacket(p))
@ -271,7 +337,7 @@ object PacketCoding {
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = {
// TODO: this is bad. rework // TODO XXX: this is bad. rework
var sequenceNumber = 0 var sequenceNumber = 0
val rawPacket : BitVector = packet match { val rawPacket : BitVector = packet match {
@ -303,10 +369,14 @@ object PacketCoding {
case default => throw new IllegalArgumentException("Unsupported packet container type") case default => throw new IllegalArgumentException("Unsupported packet container type")
} }
val packetMac = crypto.macForEncrypt(rawPacket.toByteVector) encryptPacket(crypto, sequenceNumber, rawPacket.toByteVector)
}
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, sequenceNumber : Int, rawPacket : ByteVector) : Attempt[EncryptedPacket] = {
val packetMac = crypto.macForEncrypt(rawPacket)
// opcode, payload, and MAC // opcode, payload, and MAC
val packetNoPadding = rawPacket.toByteVector ++ packetMac val packetNoPadding = rawPacket ++ packetMac
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE

View file

@ -0,0 +1,89 @@
// Copyright (c) 2016 PSForever.net to present
package psforever.net
import scodec._
import scodec.bits._
import scodec.codecs._
import shapeless._
object WorldStatus extends Enumeration {
type Type = Value
val Up, Down, Locked, Full = Value
}
object ServerType extends Enumeration {
type Type = Value
val Development, Beta, Released = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
}
object EmpireNeed extends Enumeration {
type Type = Value
val TR, NC, VS = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}
final case class WorldInformation(name : String, status : WorldStatus.Value,
serverType : ServerType.Value, empireNeed : EmpireNeed.Value)
final case class VNLWorldStatusMessage(welcomeMessage : String, worlds : Vector[WorldInformation])
extends PlanetSideGamePacket {
type Packet = VNLWorldStatusMessage
def opcode = GamePacketOpcode.VNLWorldStatusMessage
def encode = VNLWorldStatusMessage.encode(this)
}
object VNLWorldStatusMessage extends Marshallable[VNLWorldStatusMessage] {
type InStruct = WorldStatus.Value :: ServerType.Value :: HNil
type OutStruct = Int :: ServerType.Value :: Int :: HNil
implicit val statusCodec : Codec[InStruct] = {
def from(a : InStruct) : OutStruct = a match {
case status :: svrType :: HNil =>
status match {
case WorldStatus.Down =>
0 :: svrType :: 2 :: HNil
case WorldStatus.Locked =>
0 :: svrType :: 1 :: HNil
case WorldStatus.Up =>
1 :: svrType :: 0 :: HNil
case WorldStatus.Full =>
5 :: svrType :: 0 :: HNil
}
}
def to(a : OutStruct) : InStruct = a match {
case status2 :: svrType :: status1 :: HNil =>
if(status1 == 0) {
if(status2 >= 5) {
WorldStatus.Full :: svrType :: HNil
} else {
WorldStatus.Up :: svrType :: HNil
}
} else {
if(status1 != 1)
WorldStatus.Down :: svrType :: HNil
else
WorldStatus.Locked :: svrType :: HNil
}
}
(("status2" | uint16L) ::
("server_type" | ServerType.codec) ::
("status1" | uint8L)).xmap(to, from)
}
implicit val codec : Codec[VNLWorldStatusMessage] = (
("welcome_message" | PacketHelpers.encodedWideString) ::
("worlds" | vectorOfN(uint8L, (
// XXX: this needs to be limited to 0x20 bytes
// XXX: this needs to be byte aligned, but not sure how to do this
("world_name" | PacketHelpers.encodedString) :: (
("status_and_type" | statusCodec) :+
("unknown" | constant(hex"01459e25403775")) :+
("empire_need" | EmpireNeed.codec)
)
).as[WorldInformation]
))).as[VNLWorldStatusMessage]
}

View file

@ -1,6 +1,6 @@
import org.specs2.mutable._ import org.specs2.mutable._
import psforever.crypto.CryptoInterface import psforever.crypto.CryptoInterface
import psforever.crypto.CryptoInterface.{CryptoState, CryptoDHState} import psforever.crypto.CryptoInterface.CryptoDHState
import scodec.bits._ import scodec.bits._
class CryptoInterfaceTest extends Specification { class CryptoInterfaceTest extends Specification {

View file

@ -3,7 +3,7 @@ import psforever.net._
import scodec.Codec import scodec.Codec
import scodec.bits._ import scodec.bits._
class CryptoPackets extends Specification { class CryptoPacketTest extends Specification {
"PlanetSide crypto packet" in { "PlanetSide crypto packet" in {
val cNonce = 656287232 val cNonce = 656287232

View file

@ -0,0 +1,54 @@
// Copyright (c) 2016 PSForever.net to present
import org.specs2.mutable._
import psforever.net._
import scodec.bits._
class GamePacketTest extends Specification {
"PlanetSide game packet" in {
val cNonce = 656287232
"VNLWorldStatusMessage" should {
val string = hex"0597570065006c0063006f006d006500200074006f00200050006c0061006e00650074005300690064006500210020000186" ++
hex"67656d696e69" ++ hex"0100 01 00 01459e2540 3775" ++ bin"01".toByteVector
"decode" in {
PacketCoding.DecodePacket(string).require match {
case VNLWorldStatusMessage(message, worlds) =>
worlds.length mustEqual 1
message mustEqual "Welcome to PlanetSide! "
worlds{0}.name mustEqual "gemini"
worlds{0}.empireNeed mustEqual EmpireNeed.NC
worlds{0}.status mustEqual WorldStatus.Up
case default =>
true mustEqual false
}
}
"encode" in {
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(WorldInformation("gemini", WorldStatus.Up, ServerType.Beta, EmpireNeed.NC)))
//0100 04 00 01459e2540377540
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
"encode and decode multiple worlds" in {
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(
WorldInformation("PSForever1", WorldStatus.Up, ServerType.Released, EmpireNeed.NC),
WorldInformation("PSForever2", WorldStatus.Down, ServerType.Beta, EmpireNeed.TR)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
println(pkt)
true mustEqual true
}
}
}
}

View file

@ -1,5 +1,4 @@
import org.specs2.mutable._ import org.specs2.mutable._
import psforever.crypto.CryptoInterface
import psforever.net._ import psforever.net._
import scodec.bits._ import scodec.bits._

View file

@ -8,11 +8,11 @@ import scodec.{Err, Attempt, Codec}
import scodec.codecs.{uint16L, uint8L, bytes} import scodec.codecs.{uint16L, uint8L, bytes}
import java.security.SecureRandom import java.security.SecureRandom
/*sealed trait SessionState extends Serializable /**
final case class NewSession() extends SessionState * Actor that stores crypto state for a connection and filters away any packet metadata.
final case class EstablishSecureChannel() extends SessionState * Also decrypts and handles packet retries using the sequence numbers.
final case class SessionDead() extends SessionState*/ * @param session Per session state
*/
class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging { class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging {
var cryptoDHState = new CryptoInterface.CryptoDHState() var cryptoDHState = new CryptoInterface.CryptoDHState()
var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None
@ -27,10 +27,11 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
var clientChallenge = ByteVector.empty var clientChallenge = ByteVector.empty
var clientChallengeResult = ByteVector.empty var clientChallengeResult = ByteVector.empty
def receive = clientStart def receive = NewClient
def clientStart : Receive = { def NewClient : Receive = {
case RawPacket(msg) => case RawPacket(msg) =>
// PacketCoding.DecodePacket
PacketCoding.UnmarshalPacket(msg) match { PacketCoding.UnmarshalPacket(msg) match {
case Failure(e) => log.error("Could not decode packet: " + e) case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) => case Successful(p) =>
@ -39,7 +40,8 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
p match { p match {
case ControlPacket(_, ClientStart(nonce)) => case ControlPacket(_, ClientStart(nonce)) =>
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt())))) sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
context.become(clientXchg)
context.become(CryptoExchange)
case default => case default =>
log.error("Unexpected packet type " + p) log.error("Unexpected packet type " + p)
} }
@ -47,7 +49,7 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
case default => log.error(s"Invalid message received ${default}") case default => log.error(s"Invalid message received ${default}")
} }
def clientXchg : Receive = { def CryptoExchange : Receive = {
case RawPacket(msg) => case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match { PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
case Failure(e) => log.error("Could not decode packet: " + e) case Failure(e) => log.error("Could not decode packet: " + e)
@ -79,14 +81,14 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
// save the sent packet a MAC check // save the sent packet a MAC check
serverMACBuffer ++= sentPacket.drop(3) serverMACBuffer ++= sentPacket.drop(3)
context.become(clientFinished) context.become(CryptoSetupFinishing)
case default => log.error("Unexpected packet type " + p) case default => log.error("Unexpected packet type " + p)
} }
} }
case default => log.error(s"Invalid message received ${default}") case default => log.error(s"Invalid message received ${default}")
} }
def clientFinished : Receive = { def CryptoSetupFinishing : Receive = {
case RawPacket(msg) => case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match { PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
case Failure(e) => log.error("Could not decode packet: " + e) case Failure(e) => log.error("Could not decode packet: " + e)
@ -163,14 +165,14 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
sendResponse(packet) sendResponse(packet)
context.become(established) context.become(Established)
case default => failWithError("Unexpected packet type " + default) case default => failWithError("Unexpected packet type " + default)
} }
} }
case default => failWithError(s"Invalid message received ${default}") case default => failWithError(s"Invalid message received ${default}")
} }
def established : Receive = { def Established : Receive = {
case RawPacket(msg) => case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg) match { PacketCoding.UnmarshalPacket(msg) match {
case Successful(p) => case Successful(p) =>
@ -204,6 +206,15 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
))).require ))).require
sendResponse(packet) sendResponse(packet)
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(
WorldInformation("gemini", WorldStatus.Up, ServerType.Released, EmpireNeed.NC)
))
sendResponse(PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(4,
msg
)).require)
case Failure(e) => println("Failed to decode inner packet " + e) case Failure(e) => println("Failed to decode inner packet " + e)
} }
} }