From b8ff34c0f9a3edcb78c8c51ba8faad4ec7264d28 Mon Sep 17 00:00:00 2001 From: Chord Date: Fri, 4 Mar 2016 13:00:03 -0500 Subject: [PATCH] [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. --- .../psforever/net/ControlPacketOpcode.scala | 7 +- .../psforever/net/GamePacketOpcode.scala | 12 +- .../main/scala/psforever/net/PSPacket.scala | 91 +++++++--- .../scala/psforever/net/PacketCoding.scala | 170 ++++++++++++------ .../psforever/net/VNLWorldStatusMessage.scala | 89 +++++++++ .../src/test/scala/CryptoInterfaceTest.scala | 2 +- .../src/test/scala/CryptoPacketTest.scala | 2 +- common/src/test/scala/GamePacketTest.scala | 54 ++++++ common/src/test/scala/PacketCodingTest.scala | 1 - .../src/main/scala/LoginSessionActor.scala | 37 ++-- 10 files changed, 363 insertions(+), 102 deletions(-) create mode 100644 common/src/main/scala/psforever/net/VNLWorldStatusMessage.scala rename {pslogin => common}/src/test/scala/CryptoInterfaceTest.scala (98%) rename pslogin/src/test/scala/CryptoPackets.scala => common/src/test/scala/CryptoPacketTest.scala (98%) create mode 100644 common/src/test/scala/GamePacketTest.scala diff --git a/common/src/main/scala/psforever/net/ControlPacketOpcode.scala b/common/src/main/scala/psforever/net/ControlPacketOpcode.scala index 7e6786063..a5fb1076f 100644 --- a/common/src/main/scala/psforever/net/ControlPacketOpcode.scala +++ b/common/src/main/scala/psforever/net/ControlPacketOpcode.scala @@ -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}")) } - val storageType = 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) + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L) } diff --git a/common/src/main/scala/psforever/net/GamePacketOpcode.scala b/common/src/main/scala/psforever/net/GamePacketOpcode.scala index 29b5bacc1..fddf9997a 100644 --- a/common/src/main/scala/psforever/net/GamePacketOpcode.scala +++ b/common/src/main/scala/psforever/net/GamePacketOpcode.scala @@ -8,14 +8,14 @@ object GamePacketOpcode extends Enumeration { type Type = Value val - // Opcodes should have a marker every 10 + // Opcodes should have a marker every 10 (decimal) // OPCODE 0 Unknown0, LoginMessage, LoginRespMessage, Unknown3, ConnectToWorldMessage, - Unknown5, + VNLWorldStatusMessage, UnknownMessage6, UnknownMessage7, PlayerStateMessage, @@ -33,13 +33,9 @@ object GamePacketOpcode extends Enumeration { def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match { case LoginMessage => psforever.net.LoginMessage.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}")) } - val storageType = 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) + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L) } diff --git a/common/src/main/scala/psforever/net/PSPacket.scala b/common/src/main/scala/psforever/net/PSPacket.scala index b2705e963..638999949 100644 --- a/common/src/main/scala/psforever/net/PSPacket.scala +++ b/common/src/main/scala/psforever/net/PSPacket.scala @@ -1,5 +1,7 @@ package psforever.net +import java.nio.charset.Charset + import scodec.{DecodeResult, Err, Codec, Attempt} import scodec.bits._ import scodec.codecs._ @@ -14,22 +16,22 @@ sealed trait PlanetSidePacket extends Serializable { } // Used by companion objects to create encoders and decoders -sealed trait Marshallable[T] { +trait Marshallable[T] { implicit val codec : Codec[T] def encode(a : T) : Attempt[BitVector] = codec.encode(a) // 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 } -sealed trait PlanetSideControlPacket extends PlanetSidePacket { +trait PlanetSideControlPacket extends PlanetSidePacket { def opcode : ControlPacketOpcode.Type } -sealed trait PlanetSideCryptoPacket extends PlanetSidePacket { +trait PlanetSideCryptoPacket extends PlanetSidePacket { def opcode : CryptoPacketOpcode.Type } @@ -143,6 +145,17 @@ object LoginMessage extends Marshallable[LoginMessage] { 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] = { type InStruct = Either[String :: String :: HNil, String :: String :: HNil] @@ -307,12 +320,7 @@ object PacketType extends Enumeration(1) { type Type = Value val ResetSequence, Unknown2, Crypto, Normal = Value - val storageType = 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) + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint4L) } 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 -// Notes: https://mpilquist.github.io/blog/2013/06/09/scodec-part-3/ -// https://stackoverflow.com/questions/29585649/using-nested-case-classes-with-scodec -// https://gist.github.com/travisbrown/3945529 +/*class MarshallableEnum[+T] extends Enumeration { + type StorageType = Codec[Int] + + 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 { def emptyCodec[T](instance : T) = { @@ -339,13 +353,14 @@ object PacketHelpers { Codec[HNil].xmap[T](from, to) } + def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = { type Struct = Int :: HNil val struct: Codec[Struct] = storageCodec.hlist // 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), - this.getClass.getCanonicalName + ": maxId exceeds primitive type") + enum.getClass.getCanonicalName + ": maxId exceeds primitive type") def to(pkt: E#Value): Struct = { pkt.id :: HNil @@ -372,15 +387,47 @@ object PacketHelpers { private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)). xmap[Int]( (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)). - xmap[Int]( - (a : Either[Int, Int]) => a.fold[Int](a => a, a => a), - (a : Int) => if(a > 0x7f) Left(a) else Right(a) - ) <~ ignore(pad) + /*private def encodedStringSizeWithLimit(limit : Int) : Codec[Int] = { + either(bool, uint(15), uint(7)). + exmap[Int]( + (a : Either[Int, Int]) => { + 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 encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), 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) } diff --git a/common/src/main/scala/psforever/net/PacketCoding.scala b/common/src/main/scala/psforever/net/PacketCoding.scala index 9fb8a3d70..fa0d1f90f 100644 --- a/common/src/main/scala/psforever/net/PacketCoding.scala +++ b/common/src/main/scala/psforever/net/PacketCoding.scala @@ -7,32 +7,41 @@ import scodec.bits._ import scodec.{DecodeResult, Err, Attempt, Codec} import scodec.codecs.{uint16L, uint8L, uint4L, bytes} -// Packet containers +/// Packet container base trait 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, 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, packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer +/// A sequenced game packet with an opcode and payload final case class GamePacket(opcode : GamePacketOpcode.Value, sequenceNumber : Int, packet : PlanetSideGamePacket) extends PlanetSidePacketContainer -// Just an opcode + payload (does not expect a response) +/// Just an opcode + payload final case class ControlPacket(opcode : ControlPacketOpcode.Value, packet : PlanetSideControlPacket) extends PlanetSidePacketContainer object PacketCoding { + /// A lower bound on the packet size 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] = { // check for a minimum length 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] = { // check for a minimum length if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) @@ -68,70 +93,110 @@ object PacketCoding { var opcodeEncoded : 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 { case GamePacket(opcode, seq, payload) => - val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false) - flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + secured = false + packetType = PacketType.Normal + sequenceNum = seq - GamePacketOpcode.codec.encode(opcode) match { - case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) - 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)) + EncodePacket(payload) match { + case f @ Failure(e) => return f case Successful(p) => payloadEncoded = p } case ControlPacket(opcode, payload) => - flagsEncoded = hex"00".bits + controlPacket = true - ControlPacketOpcode.codec.encode(opcode) match { - case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) - case Successful(p) => opcodeEncoded = p - } - - encodePacket(payload) match { - case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) + EncodePacket(payload) match { + case f @ Failure(e) => return f case Successful(p) => payloadEncoded = p } case CryptoPacket(seq, payload) => - val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false) - flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + secured = false + packetType = PacketType.Crypto + sequenceNum = seq - 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 - } - - encodePacket(payload) match { - case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $payload: " + e.messageWithContext)) + EncodePacket(payload) match { + case f @ Failure(e) => return f case Successful(p) => payloadEncoded = p } case EncryptedPacket(seq, payload) => - val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true) - flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + secured = true + packetType = PacketType.Normal + sequenceNum = seq // 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 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 } + 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 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 CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet) def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet) @@ -199,6 +264,7 @@ object PacketCoding { val packet = DecodeControlPacket(msg) packet match { + // just return the failure case f @ Failure(e) => f case Successful(p) => Attempt.successful(CreateControlPacket(p)) @@ -271,7 +337,7 @@ object PacketCoding { /////////////////////////////////////////////////////////// def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { - // TODO: this is bad. rework + // TODO XXX: this is bad. rework var sequenceNumber = 0 val rawPacket : BitVector = packet match { @@ -303,10 +369,14 @@ object PacketCoding { 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 - val packetNoPadding = rawPacket.toByteVector ++ packetMac + val packetNoPadding = rawPacket ++ packetMac val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE diff --git a/common/src/main/scala/psforever/net/VNLWorldStatusMessage.scala b/common/src/main/scala/psforever/net/VNLWorldStatusMessage.scala new file mode 100644 index 000000000..770b4aa29 --- /dev/null +++ b/common/src/main/scala/psforever/net/VNLWorldStatusMessage.scala @@ -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] +} diff --git a/pslogin/src/test/scala/CryptoInterfaceTest.scala b/common/src/test/scala/CryptoInterfaceTest.scala similarity index 98% rename from pslogin/src/test/scala/CryptoInterfaceTest.scala rename to common/src/test/scala/CryptoInterfaceTest.scala index ac5b3da2e..4a7af47be 100644 --- a/pslogin/src/test/scala/CryptoInterfaceTest.scala +++ b/common/src/test/scala/CryptoInterfaceTest.scala @@ -1,6 +1,6 @@ import org.specs2.mutable._ import psforever.crypto.CryptoInterface -import psforever.crypto.CryptoInterface.{CryptoState, CryptoDHState} +import psforever.crypto.CryptoInterface.CryptoDHState import scodec.bits._ class CryptoInterfaceTest extends Specification { diff --git a/pslogin/src/test/scala/CryptoPackets.scala b/common/src/test/scala/CryptoPacketTest.scala similarity index 98% rename from pslogin/src/test/scala/CryptoPackets.scala rename to common/src/test/scala/CryptoPacketTest.scala index 917ca9196..5c9f9e9f9 100644 --- a/pslogin/src/test/scala/CryptoPackets.scala +++ b/common/src/test/scala/CryptoPacketTest.scala @@ -3,7 +3,7 @@ import psforever.net._ import scodec.Codec import scodec.bits._ -class CryptoPackets extends Specification { +class CryptoPacketTest extends Specification { "PlanetSide crypto packet" in { val cNonce = 656287232 diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala new file mode 100644 index 000000000..49d94c45b --- /dev/null +++ b/common/src/test/scala/GamePacketTest.scala @@ -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 + } + } + + } +} diff --git a/common/src/test/scala/PacketCodingTest.scala b/common/src/test/scala/PacketCodingTest.scala index f819c9ee7..f34879337 100644 --- a/common/src/test/scala/PacketCodingTest.scala +++ b/common/src/test/scala/PacketCodingTest.scala @@ -1,5 +1,4 @@ import org.specs2.mutable._ -import psforever.crypto.CryptoInterface import psforever.net._ import scodec.bits._ diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index 68af1ddc9..63b3387dd 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -8,11 +8,11 @@ import scodec.{Err, Attempt, Codec} import scodec.codecs.{uint16L, uint8L, bytes} import java.security.SecureRandom -/*sealed trait SessionState extends Serializable -final case class NewSession() extends SessionState -final case class EstablishSecureChannel() extends SessionState -final case class SessionDead() extends SessionState*/ - +/** + * Actor that stores crypto state for a connection and filters away any packet metadata. + * Also decrypts and handles packet retries using the sequence numbers. + * @param session Per session state + */ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging { var cryptoDHState = new CryptoInterface.CryptoDHState() var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None @@ -27,10 +27,11 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging var clientChallenge = ByteVector.empty var clientChallengeResult = ByteVector.empty - def receive = clientStart + def receive = NewClient - def clientStart : Receive = { + def NewClient : Receive = { case RawPacket(msg) => + // PacketCoding.DecodePacket PacketCoding.UnmarshalPacket(msg) match { case Failure(e) => log.error("Could not decode packet: " + e) case Successful(p) => @@ -39,7 +40,8 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging p match { case ControlPacket(_, ClientStart(nonce)) => sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt())))) - context.become(clientXchg) + + context.become(CryptoExchange) case default => 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}") } - def clientXchg : Receive = { + def CryptoExchange : Receive = { case RawPacket(msg) => PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match { 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 serverMACBuffer ++= sentPacket.drop(3) - context.become(clientFinished) + context.become(CryptoSetupFinishing) case default => log.error("Unexpected packet type " + p) } } case default => log.error(s"Invalid message received ${default}") } - def clientFinished : Receive = { + def CryptoSetupFinishing : Receive = { case RawPacket(msg) => PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match { case Failure(e) => log.error("Could not decode packet: " + e) @@ -163,14 +165,14 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging sendResponse(packet) - context.become(established) + context.become(Established) case default => failWithError("Unexpected packet type " + default) } } case default => failWithError(s"Invalid message received ${default}") } - def established : Receive = { + def Established : Receive = { case RawPacket(msg) => PacketCoding.UnmarshalPacket(msg) match { case Successful(p) => @@ -204,6 +206,15 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging ))).require 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) } }