diff --git a/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala b/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala index 09f806b93..7c63e573a 100644 --- a/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala @@ -49,8 +49,8 @@ object ControlPacketOpcode extends Enumeration { Unknown30 = Value - private def noDecoder(opcode : ControlPacketOpcode.Type) = (a : BitVector) => - Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}")) + private def noDecoder(opcode : ControlPacketOpcode.Type) = (_ : BitVector) => + Attempt.failure(Err(s"Could not find a marshaller for control packet $opcode")) def getPacketDecoder(opcode : ControlPacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideControlPacket]] = (opcode.id : @switch) match { // OPCODES 0x00-0f @@ -74,11 +74,11 @@ object ControlPacketOpcode extends Enumeration { // OPCODES 0x10-1e case 0x10 => SlottedMetaPacket.decodeWithOpcode(SlottedMetaPacket7) - case 0x11 => noDecoder(RelatedA0) + case 0x11 => control.RelatedA0.decode case 0x12 => noDecoder(RelatedA1) case 0x13 => noDecoder(RelatedA2) case 0x14 => noDecoder(RelatedA3) - case 0x15 => noDecoder(RelatedB0) + case 0x15 => control.RelatedB0.decode case 0x16 => noDecoder(RelatedB1) case 0x17 => noDecoder(RelatedB2) // 0x18 @@ -89,7 +89,7 @@ object ControlPacketOpcode extends Enumeration { case 0x1c => noDecoder(Unknown28) case 0x1d => control.ConnectionClose.decode case 0x1e => noDecoder(Unknown30) - case default => noDecoder(opcode) + case _ => noDecoder(opcode) } implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L) diff --git a/common/src/main/scala/net/psforever/packet/PacketCoding.scala b/common/src/main/scala/net/psforever/packet/PacketCoding.scala index f3500b3e3..fa2400e8e 100644 --- a/common/src/main/scala/net/psforever/packet/PacketCoding.scala +++ b/common/src/main/scala/net/psforever/packet/PacketCoding.scala @@ -2,13 +2,13 @@ package net.psforever.packet import net.psforever.crypto.CryptoInterface -import scodec.Attempt.{Successful, Failure} +import scodec.Attempt.{Failure, Successful} import scodec.bits._ -import scodec.{Err, Attempt, Codec} -import scodec.codecs.{uint16L, uint8L, bytes} +import scodec.{Attempt, Codec, Err} +import scodec.codecs.{bytes, uint16L, uint8L} /** - * Base trait of the packet container `case class`es. + * Base trait of the packet container `case class`. */ sealed trait PlanetSidePacketContainer @@ -81,7 +81,7 @@ object PacketCoding { /* Marshalling and Encoding. */ /** - * Transforms a type of packet into the `BitVector` representations of its component data and then reconstructs those components. + * Transform a kind of packet into the sequence of data that represents it. * Wraps around the encoding process for all valid packet container types. * @param packet the packet to encode * @return a `BitVector` translated from the packet's data @@ -140,12 +140,11 @@ object PacketCoding { case Successful(p) => seqEncoded = p } } - Attempt.successful(flagsEncoded ++ seqEncoded ++ paddingEncoded ++ payloadEncoded) } /** - * Overloaded method for transforming a control packet into its `BitVector` representation. + * Overloaded method for transforming a `ControlPacket` into its `BitVector` representation. * @param packet the control packet to encode * @return a `BitVector` translated from the packet's data */ @@ -156,7 +155,6 @@ object PacketCoding { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in control packet $opcode: " + e.messageWithContext)) case Successful(p) => opcodeEncoded = p } - var payloadEncoded = BitVector.empty encodePacket(packet) match { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext)) @@ -166,7 +164,7 @@ object PacketCoding { } /** - * Overloaded method for transforming a crypto packet into its `BitVector` representation. + * Overloaded method for transforming a `CryptoPacket` into its `BitVector` representation. * @param packet the crypto packet to encode * @return a `BitVector` translated from the packet's data */ @@ -178,7 +176,7 @@ object PacketCoding { } /** - * Overloaded method for transforming a game packet into its `BitVector` representation. + * Overloaded method for transforming a `GamePacket` into its `BitVector` representation. * @param packet the game packet to encode * @return a `BitVector` translated from the packet's data */ @@ -189,7 +187,6 @@ object PacketCoding { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in game packet $opcode: " + e.messageWithContext)) case Successful(p) => opcodeEncoded = p } - var payloadEncoded = BitVector.empty encodePacket(packet) match { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext)) @@ -199,7 +196,7 @@ object PacketCoding { } /** - * Calls the packet-specific encode function. + * Calls the packet's own `encode` function. * Lowest encode call before the packet-specific implementations. * @param packet the packet to encode * @return a `BitVector` translated from the packet's data @@ -214,12 +211,9 @@ object PacketCoding { final val PLANETSIDE_MIN_PACKET_SIZE = 1 /** - * Transforms `BitVector` data into a PlanetSide packet.
- *
- * 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. + * Transforms `ByteVector` data into a PlanetSide packet. + * Attempt to decode with an optional header and required payload. + * Does not decode into a `GamePacket`. * @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 @@ -237,18 +231,18 @@ 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. + * Helper function to decode a packet without specifying a crypto state. + * Used when there is no crypto state available such as in tests. * @param msg packet data bytes * @return `PlanetSidePacketContainer` */ def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = UnmarshalPacket(msg, CryptoPacketOpcode.Ignore) /** - * Handle decoding for a packet that has been identified as not a control packet. - * It may just be encrypted or it may be involved in the encryption process itself. + * Handle decoding for a packet that has been identified as not a `ControlPacket`. + * It may just be encrypted (`EncryptedPacket`) or it may be involved in the encryption process itself (`CryptoPacket`). * @param msg the packet - * @param cryptoState the current state of the connection's crypto + * @param cryptoState the current cryptographic state * @return a `PlanetSidePacketContainer` */ private def unmarshalFlaggedPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { @@ -258,7 +252,6 @@ object PacketCoding { return Attempt.failure(Err("Failed to parse packet flags: " + e.message)) case _ => } - val flags = decodedFlags.require.value val packetType = flags.packetType packetType match { @@ -275,7 +268,6 @@ object PacketCoding { case _ => return Attempt.failure(Err("Unsupported packet type: " + flags.packetType.toString)) } - //all packets have a two byte sequence ID val decodedSeq = uint16L.decode(decodedFlags.require.remainder) //TODO: make this a codec for reuse decodedSeq match { @@ -285,7 +277,6 @@ object PacketCoding { } val sequence = decodedSeq.require.value val payload = decodedSeq.require.remainder.toByteVector - packetType match { case PacketType.Crypto => unmarshalCryptoPacket(cryptoState, sequence, payload) @@ -295,35 +286,37 @@ object PacketCoding { } /** - * Handle decoding for a control packet. + * Handle decoding for a `ControlPacket`. * @param msg the packet * @return a `ControlPacket` */ private def unmarshalControlPacket(msg : ByteVector) : Attempt[ControlPacket] = { DecodeControlPacket(msg) match { - case f @ Failure(_) => f + case f @ Failure(_) => + f case Successful(p) => Attempt.successful(CreateControlPacket(p)) } } /** - * Handle decoding for a game packet. + * Handle decoding for a `GamePacket`. * @param sequence na * @param msg the packet data * @return a `GamePacket` */ private def unmarshalGamePacket(sequence : Int, msg : ByteVector) : Attempt[GamePacket] = { DecodeGamePacket(msg) match { - case f @ Failure(_) => f + case f @ Failure(_) => + f case Successful(p) => Attempt.successful(CreateGamePacket(sequence, p)) } } /** - * Handle decoding for a crypto packet. - * @param state the current state of the connection's crypto + * Handle decoding for a `CryptoPacket`. + * @param state the current cryptographic state * @param sequence na * @param payload the packet data * @return a `CryptoPacket` @@ -338,8 +331,8 @@ object PacketCoding { } /** - * Handle decoding for an encrypted packet. - * That is, it's already encrypted. + * Handle decoding for an `EncryptedPacket`. + * The payload is already encrypted. * Just repackage the data. * @param sequence na * @param payload the packet data @@ -350,15 +343,17 @@ object PacketCoding { } /** - * Similar to `UnmarshalPacket`, but does not process any packet header and does not support decoding of crypto packets. + * Transforms `ByteVector` data into a PlanetSide packet. + * Similar to the `UnmarshalPacket` but it does not process packet headers. + * It supports `GamePacket` in exchange for not supporting `CryptoPacket` (like `UnMarshalPacket`). * Mostly used in tests. * @param msg raw, unencrypted packet * @return `PlanetSidePacket` + * @see `UnMarshalPacket` */ def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = { if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")) - val firstByte = msg{0} firstByte match { case 0x00 => DecodeControlPacket(msg.drop(1)) //control packets dont need the first byte @@ -367,7 +362,7 @@ object PacketCoding { } /** - * Transform a `BitVector` into a control packet. + * Transform a `ByteVector` into a `ControlPacket`. * @param msg the the raw data to decode * @return a `PlanetSideControlPacket` */ @@ -388,7 +383,7 @@ object PacketCoding { } /** - * Transform a `BitVector` into a game packet. + * Transform a `ByteVector` into a `GamePacket`. * @param msg the the raw data to decode * @return a `PlanetSideGamePacket` */ @@ -411,28 +406,45 @@ object PacketCoding { /* Encrypting and Decrypting. */ /** - * Encrypt the provided packet using the provided crypto state. - * @param crypto the current state of the connection's crypto + * Transform the privileged `packet` into a `RawPacket` representation to get: + * the sequence number, + * and the raw `ByteVector` data. * @param packet the unencrypted packet - * @return an `EncryptedPacket` + * @return paired data based on the packet */ - def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { + def getPacketDataForEncryption(packet : PlanetSidePacketContainer) : Attempt[(Int, ByteVector)] = { makeRawPacket(packet) match { case Successful(rawPacket) => - var sequenceNumber = 0 - packet match { //the sequence is a not default if this is a GamePacket - case GamePacket(_, seq, _) => sequenceNumber = seq - case _ => ; + val sequenceNumber = packet match { //the sequence is variable if this is a GamePacket + case GamePacket(_, seq, _) => seq + case _ => 0 } - encryptPacket(crypto, sequenceNumber, rawPacket.toByteVector) + Successful((sequenceNumber, rawPacket.toByteVector)) - case f @ Failure(_) => f; + case f @ Failure(_) => + f } } /** - * Transform either a game packet or a control packet into a `BitVector`. - * This is more thorough than the process of unmarshalling, though the results are very similar. + * Encrypt the provided packet using the provided cryptographic state. + * Translate the packet into data to send on to the actual encryption process. + * @param crypto the current cryptographic state + * @param packet the unencrypted packet + * @return an `EncryptedPacket` + */ + def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { + getPacketDataForEncryption(packet) match { + case Successful((seq, data)) => + encryptPacket(crypto, seq, data) + case f @ Failure(_) => + f + } + } + + /** + * Transform either a `GamePacket` or a `ControlPacket` into a `BitVector`. + * This is not as thorough as the process of unmarshalling though the results are very similar. * @param packet a packet * @return a `BitVector` that represents the packet */ @@ -440,7 +452,8 @@ object PacketCoding { case GamePacket(opcode, _, payload) => val opcodeEncoded = GamePacketOpcode.codec.encode(opcode) opcodeEncoded match { - case Failure(e) => Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.message)) + case Failure(e) => + Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.message)) case _ => encodePacket(payload) match { case Failure(e) => Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) @@ -451,7 +464,8 @@ object PacketCoding { case ControlPacket(opcode, payload) => val opcodeEncoded = ControlPacketOpcode.codec.encode(opcode) opcodeEncoded match { - case Failure(e) => Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) + case Failure(e) => + Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) case _ => encodePacket(payload) match { case Failure(e) => Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) @@ -465,10 +479,10 @@ object PacketCoding { /** * Perform encryption on the packet's raw data. - * @param crypto the current state of the connection's crypto + * @param crypto the current cryptographic state * @param sequenceNumber na * @param rawPacket a `ByteVector` that represents the packet data - * @return + * @return an `EncryptedPacket` */ def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, sequenceNumber : Int, rawPacket : ByteVector) : Attempt[EncryptedPacket] = { val packetMac = crypto.macForEncrypt(rawPacket) @@ -482,12 +496,39 @@ object PacketCoding { } /** - * Perform decryption on a packet's data. + * Perform decryption on an `EncryptedPacket`. * @param crypto the current state of the connection's crypto * @param packet an encrypted packet - * @return + * @return a general packet container type */ def decryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : EncryptedPacket) : Attempt[PlanetSidePacketContainer] = { + decryptPacketData(crypto, packet) match { + case Successful(payload) => unmarshalPayload(packet.sequenceNumber, payload) + case f @ Failure(_) => f + } + } + + /** + * Transform decrypted packet data into the type of packet that it represents. + * Will not compose data into an `EncryptedPacket` or into a `CryptoPacket`. + * @param sequenceNumber na + * @param payload the decrypted packet data + * @return a general packet container type + */ + def unmarshalPayload(sequenceNumber : Int, payload : ByteVector) : Attempt[PlanetSidePacketContainer] = { + payload{0} match { + case 0x00 => unmarshalControlPacket(payload.drop(1)) + case _ => unmarshalGamePacket(sequenceNumber, payload) + } + } + + /** + * Compose the decrypted payload data from a formerly encrypted packet. + * @param crypto the current state of the connection's crypto + * @param packet an encrypted packet + * @return a sequence of decrypted data + */ + def decryptPacketData(crypto : CryptoInterface.CryptoStateWithMAC, packet : EncryptedPacket) : Attempt[ByteVector] = { val payloadDecrypted = crypto.decrypt(packet.payload) val payloadJustLen = payloadDecrypted.takeRight(1) //get the last byte which is the padding length val padding = uint8L.decode(payloadJustLen.bits) @@ -523,9 +564,6 @@ object PacketCoding { if(payloadNoMac.length < PLANETSIDE_MIN_PACKET_SIZE) { return Attempt.failure(Err(s"Decrypted packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")) } - payloadNoMac{0} match { - case 0x00 => unmarshalControlPacket(payloadNoMac.drop(1)) - case _ => unmarshalGamePacket(packet.sequenceNumber, payloadNoMac) - } + Successful(payloadNoMac) } } diff --git a/common/src/main/scala/net/psforever/packet/control/ControlSync.scala b/common/src/main/scala/net/psforever/packet/control/ControlSync.scala index 85fc7cc0f..e38617969 100644 --- a/common/src/main/scala/net/psforever/packet/control/ControlSync.scala +++ b/common/src/main/scala/net/psforever/packet/control/ControlSync.scala @@ -5,9 +5,27 @@ import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideContro import scodec.Codec import scodec.codecs._ -final case class ControlSync(timeDiff : Int, unk : Long, - field1 : Long, field2 : Long, field3 : Long, field4 : Long, - field64A : Long, field64B : Long) +/** + * Dispatched by the client periodically (approximately once every ten seconds). + * @param timeDiff the exact number of milliseconds since the last `ControlSync` packet + * @param unk na + * @param field1 na + * @param field2 na + * @param field3 na + * @param field4 na + * @param field64A na; + * increments by 41 per packet + * @param field64B na; + * increments by 21 per packet + */ +final case class ControlSync(timeDiff : Int, + unk : Long, + field1 : Long, + field2 : Long, + field3 : Long, + field4 : Long, + field64A : Long, + field64B : Long) extends PlanetSideControlPacket { type Packet = ControlSync def opcode = ControlPacketOpcode.ControlSync diff --git a/common/src/main/scala/net/psforever/packet/control/ControlSyncResp.scala b/common/src/main/scala/net/psforever/packet/control/ControlSyncResp.scala index f3f0f4e8b..1737629ec 100644 --- a/common/src/main/scala/net/psforever/packet/control/ControlSyncResp.scala +++ b/common/src/main/scala/net/psforever/packet/control/ControlSyncResp.scala @@ -5,8 +5,27 @@ import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideContro import scodec.Codec import scodec.codecs._ -final case class ControlSyncResp(timeDiff : Int, serverTick : Long, - field1 : Long, field2 : Long, field3 : Long, field4 : Long) +/** + * The response packet dispatched by the server to a client's `ControlSync` packet. + * As noted, it echoes most of the fields originating from within its companion packet except for `serverTick`. + * @param timeDiff na; + * echoes `ControlSync.timeDiff` + * @param serverTick na + * @param field1 na; + * echoes `ControlSync.field64A` + * @param field2 na; + * echoes `ControlSync.field64B` + * @param field3 na; + * echoes `ControlSync.field64B` (+/- 1) + * @param field4 na; + * echoes `ControlSync.field64A` + */ +final case class ControlSyncResp(timeDiff : Int, + serverTick : Long, + field1 : Long, + field2 : Long, + field3 : Long, + field4 : Long) extends PlanetSideControlPacket { type Packet = ControlSyncResp def opcode = ControlPacketOpcode.ControlSyncResp diff --git a/common/src/main/scala/net/psforever/packet/control/HandleGamePacket.scala b/common/src/main/scala/net/psforever/packet/control/HandleGamePacket.scala index 062ca6107..56ddd1288 100644 --- a/common/src/main/scala/net/psforever/packet/control/HandleGamePacket.scala +++ b/common/src/main/scala/net/psforever/packet/control/HandleGamePacket.scala @@ -3,15 +3,25 @@ package net.psforever.packet.control import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} import scodec.Codec -import scodec.bits.ByteVector +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ -final case class HandleGamePacket(packet : ByteVector) +final case class HandleGamePacket(len : Int, + stream : ByteVector, + rest : BitVector = BitVector.empty) extends PlanetSideControlPacket { def opcode = ControlPacketOpcode.HandleGamePacket - def encode = throw new Exception("This packet type should never be encoded") + def encode = HandleGamePacket.encode(this) } object HandleGamePacket extends Marshallable[HandleGamePacket] { - implicit val codec : Codec[HandleGamePacket] = bytes.as[HandleGamePacket].decodeOnly -} \ No newline at end of file + def apply(stream : ByteVector) : HandleGamePacket = { + new HandleGamePacket(stream.length.toInt, stream) + } + + implicit val codec : Codec[HandleGamePacket] = ( + ("len" | uint16) >>:~ { len => + ("stream" | bytes(len)) :: + ("rest" | bits) + }).as[HandleGamePacket] +} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala b/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala new file mode 100644 index 000000000..0bd34517c --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/control/RelatedA0.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.control + +import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched from the client in regards to errors trying to process prior `ControlPackets`. + * Explains which packet was in error by sending back its `subslot` number. + * @param subslot identification of a control packet + */ +final case class RelatedA0(subslot : Int) + extends PlanetSideControlPacket { + type Packet = RelatedA0 + def opcode = ControlPacketOpcode.RelatedA0 + def encode = RelatedA0.encode(this) +} + +object RelatedA0 extends Marshallable[RelatedA0] { + implicit val codec : Codec[RelatedA0] = ("subslot" | uint16).as[RelatedA0] +} diff --git a/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala b/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala new file mode 100644 index 000000000..983729639 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/control/RelatedB0.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.control + +import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched to coordinate information regarding `ControlPacket` packets between the client and server. + * When dispatched by the client, it relates the current (or last received) `SlottedMetaPacket` `subslot` number back to the server. + * When dispatched by the server, it relates ??? + * @param subslot identification of a control packet + */ +final case class RelatedB0(subslot : Int) + extends PlanetSideControlPacket { + type Packet = RelatedB0 + def opcode = ControlPacketOpcode.RelatedB0 + def encode = RelatedB0.encode(this) +} + +object RelatedB0 extends Marshallable[RelatedB0] { + implicit val codec : Codec[RelatedB0] = ("subslot" | uint16).as[RelatedB0] +} diff --git a/common/src/test/scala/ControlPacketTest.scala b/common/src/test/scala/ControlPacketTest.scala deleted file mode 100644 index 2bbe8b714..000000000 --- a/common/src/test/scala/ControlPacketTest.scala +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) 2017 PSForever - -import org.specs2.mutable._ -import org.specs2.specification -import net.psforever.packet._ -import net.psforever.packet.control._ -import org.specs2.specification.core.Fragment -import scodec.Attempt.Successful -import scodec.bits._ -import scodec.codecs.uint16 - -class ControlPacketTest extends Specification { - - "PlanetSide control packet" in { - "ControlSync" should { - val string = hex"0007 5268 0000004D 00000052 0000004D 0000007C 0000004D 0000000000000276 0000000000000275" - - "decode" in { - PacketCoding.DecodePacket(string).require match { - case ControlSync(a, b, c, d, e, f, g, h) => - a mustEqual 21096 - - b mustEqual 0x4d - c mustEqual 0x52 - d mustEqual 0x4d - e mustEqual 0x7c - f mustEqual 0x4d - - g mustEqual 0x276 - h mustEqual 0x275 - case default => - ko - } - } - - "encode" in { - val encoded = PacketCoding.EncodePacket(ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)).require - - encoded.toByteVector mustEqual string - } - } - - "ControlSyncResp" should { - val string = hex"0008 5268 21392D92 0000000000000276 0000000000000275 0000000000000275 0000000000000276" - - "decode" in { - PacketCoding.DecodePacket(string).require match { - case ControlSyncResp(a, b, c, d, e, f) => - a mustEqual 21096 - - b mustEqual 0x21392D92 - c mustEqual 0x276 - d mustEqual 0x275 - e mustEqual 0x275 - f mustEqual 0x276 - case default => - ko - } - } - - "encode" in { - val encoded = PacketCoding.EncodePacket(ControlSyncResp(21096, 0x21392D92, 0x276, 0x275, 0x275, 0x276)).require - - encoded.toByteVector mustEqual string - } - } - - "SlottedMetaPacket" should { - val string = hex"00 09 00 00 00194302484C36563130433F" ++ - hex"4C6835316369774A0000000018FABE0C" ++ - hex"00000000000000000000000001000000" ++ - hex"020000006B7BD8288C6469666671756F" ++ - hex"7469656E740000000000440597570065" ++ - hex"006C0063006F006D006500200074006F" ++ - hex"00200050006C0061006E006500740053" ++ - hex"0069006400650021002000018667656D" ++ - hex"696E690100040001459E2540377540" - - def createMetaPacket(slot : Int, subslot : Int, rest : ByteVector) = hex"00" ++ - ControlPacketOpcode.codec.encode( - ControlPacketOpcode(ControlPacketOpcode.SlottedMetaPacket0.id + slot) - ).require.toByteVector ++ uint16.encode(subslot).require.toByteVector ++ rest - - "decode as the base slot and subslot" in { - PacketCoding.DecodePacket(string).require match { - case SlottedMetaPacket(slot, subslot, rest) => - slot mustEqual 0 - subslot mustEqual 0 - rest mustEqual string.drop(4) - case default => - ko - } - } - - "decode as an arbitrary slot and subslot" in { - val maxSlots = ControlPacketOpcode.SlottedMetaPacket7.id - ControlPacketOpcode.SlottedMetaPacket0.id - - // create all possible SlottedMetaPackets - Fragment.foreach(0 to maxSlots) { i => - "slot " + i ! { - val subslot = 12323 - val pkt = createMetaPacket(i, subslot, ByteVector.empty) - - PacketCoding.DecodePacket(pkt).require match { - case SlottedMetaPacket(slot, subslotDecoded, rest) => - - // XXX: there isn't a simple solution to Slot0 and Slot4 be aliases of each other structurally - // This is probably best left to higher layers - //slot mustEqual i % 4 // this is seen at 0x00A3FBFA - slot mustEqual i - subslotDecoded mustEqual subslot - rest mustEqual ByteVector.empty // empty in this case - case default => - ko - } - } - } - } - - "encode" in { - val encoded = PacketCoding.EncodePacket(SlottedMetaPacket(0, 0x1000, ByteVector.empty)).require - val encoded2 = PacketCoding.EncodePacket(SlottedMetaPacket(3, 0xffff, hex"414243")).require - val encoded3 = PacketCoding.EncodePacket(SlottedMetaPacket(7, 0, hex"00")).require - - encoded.toByteVector mustEqual createMetaPacket(0, 0x1000, ByteVector.empty) - encoded2.toByteVector mustEqual createMetaPacket(3, 0xffff, hex"414243") - encoded3.toByteVector mustEqual createMetaPacket(7, 0, hex"00") - - PacketCoding.EncodePacket(SlottedMetaPacket(8, 0, hex"00")).require must throwA[AssertionError] - PacketCoding.EncodePacket(SlottedMetaPacket(-1, 0, hex"00")).require must throwA[AssertionError] - PacketCoding.EncodePacket(SlottedMetaPacket(0, 0x10000, hex"00")).require must throwA[IllegalArgumentException] - } - } - - "MultiPacketEx" should { - val strings = Vector( - hex"00", - hex"01 41", - hex"01 41" ++ hex"02 4142", - hex"fe" ++ ByteVector.fill(0xfe)(0x41), - hex"ffff00" ++ ByteVector.fill(0xff)(0x41), - hex"ff0001" ++ ByteVector.fill(0x100)(0x41), - hex"ff ffff ffff 0000" ++ ByteVector.fill(0x0000ffff)(0x41), - hex"ff ffff 0000 0100" ++ ByteVector.fill(0x00010000)(0x41) - ) - - val packets = Vector( - MultiPacketEx(Vector(ByteVector.empty)), - MultiPacketEx(Vector(hex"41")), - MultiPacketEx(Vector(hex"41", hex"4142")), - MultiPacketEx(Vector(ByteVector.fill(0xfe)(0x41))), - MultiPacketEx(Vector(ByteVector.fill(0xff)(0x41))), - MultiPacketEx(Vector(ByteVector.fill(0x100)(0x41))), - MultiPacketEx(Vector(ByteVector.fill(0x0000ffff)(0x41))), - MultiPacketEx(Vector(ByteVector.fill(0x00010000)(0x41))) - ) - - "decode" in { - Fragment.foreach(strings.indices) { i => - "test "+i ! { MultiPacketEx.decode(strings{i}.bits).require.value mustEqual packets{i} } - } - } - - "encode" in { - Fragment.foreach(packets.indices) { i => - "test "+i ! { MultiPacketEx.encode(packets{i}).require.toByteVector mustEqual strings{i} } - } - } - } - - "TeardownConnection" should { - val string = hex"00 05 02 4F 57 17 00 06" - - "decode" in { - PacketCoding.DecodePacket(string).require match { - case TeardownConnection(nonce) => - nonce mustEqual 391597826 - case default => - ko - } - } - - "encode" in { - val encoded = PacketCoding.EncodePacket(TeardownConnection(391597826)).require - - encoded.toByteVector mustEqual string - } - } - } -} \ No newline at end of file diff --git a/common/src/test/scala/control/ControlSyncRespTest.scala b/common/src/test/scala/control/ControlSyncRespTest.scala new file mode 100644 index 000000000..dec5fa700 --- /dev/null +++ b/common/src/test/scala/control/ControlSyncRespTest.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class ControlSyncRespTest extends Specification { + val string = hex"0008 5268 21392D92 0000000000000276 0000000000000275 0000000000000275 0000000000000276" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ControlSyncResp(a, b, c, d, e, f) => + a mustEqual 21096 + + b mustEqual 0x21392D92 + c mustEqual 0x276 + d mustEqual 0x275 + e mustEqual 0x275 + f mustEqual 0x276 + case _ => + ko + } + } + + "encode" in { + val encoded = PacketCoding.EncodePacket(ControlSyncResp(21096, 0x21392D92, 0x276, 0x275, 0x275, 0x276)).require + + encoded.toByteVector mustEqual string + } +} diff --git a/common/src/test/scala/control/ControlSyncTest.scala b/common/src/test/scala/control/ControlSyncTest.scala new file mode 100644 index 000000000..208e3a782 --- /dev/null +++ b/common/src/test/scala/control/ControlSyncTest.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class ControlSyncTest extends Specification { + val string = hex"0007 5268 0000004D 00000052 0000004D 0000007C 0000004D 0000000000000276 0000000000000275" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ControlSync(a, b, c, d, e, f, g, h) => + a mustEqual 21096 + b mustEqual 0x4d + c mustEqual 0x52 + d mustEqual 0x4d + e mustEqual 0x7c + f mustEqual 0x4d + g mustEqual 0x276 + h mustEqual 0x275 + case _ => + ko + } + } + + "encode" in { + val encoded = PacketCoding.EncodePacket(ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)).require + encoded.toByteVector mustEqual string + } +} diff --git a/common/src/test/scala/control/HandleGamePacketTest.scala b/common/src/test/scala/control/HandleGamePacketTest.scala new file mode 100644 index 000000000..840f7e3bb --- /dev/null +++ b/common/src/test/scala/control/HandleGamePacketTest.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class HandleGamePacketTest extends Specification { + //this is the first from a series of SlottedMetaPacket4s; the length field was modified from 12 DC to pass the test + val base = hex"18 D5 96 00 00 BC 8E 00 03 A2 16 5D A4 5F B0 80 00 04 30 40 00 08 30 46 00 4A 00 48 00 02 02 F0 62 1E 80 80 00 00 00 00 00 3F FF CC 0D 40 00 20 00 03 00 27 C3 01 C8 00 00 03 08 00 00 03 FF FF FF FC A4 04 00 00 62 00 18 02 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 C8 00 00 01 00 7E C8 00 C8 00 00 00 5D B0 81 40 00 00 00 00 00 00 00 00 00 00 00 00 02 C0 00 40 83 85 46 86 C7 07 8A 4A 80 70 0C 00 01 98 00 00 01 24 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 31 30 90 78 70 65 5F 6A 6F 69 6E 5F 70 6C 61 74 6F 6F 6E 92 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 31 34 8F 78 70 65 5F 6A 6F 69 6E 5F 6F 75 74 66 69 74 92 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 31 31 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 39 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 38 92 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 31 33 93 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 36 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 37 8E 78 70 65 5F 6A 6F 69 6E 5F 73 71 75 61 64 8C 78 70 65 5F 62 69 6E 64 5F 61 6D 73 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 35 91 78 70 65 5F 62 69 6E 64 5F 66 61 63 69 6C 69 74" + val string = hex"00 00 01 CB" ++ base + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case HandleGamePacket(len, data, extra) => + len mustEqual 459 + data mustEqual base + extra mustEqual BitVector.empty + case _ => + ko + } + } + + "encode" in { + val pkt = HandleGamePacket(base) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string + } +} diff --git a/common/src/test/scala/control/MultiPacketExTest.scala b/common/src/test/scala/control/MultiPacketExTest.scala new file mode 100644 index 000000000..e537bf81e --- /dev/null +++ b/common/src/test/scala/control/MultiPacketExTest.scala @@ -0,0 +1,43 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet.control._ +import org.specs2.specification.core.Fragment +import scodec.bits._ + +class MultiPacketExTest extends Specification { + val strings = Vector( + hex"00", + hex"01 41", + hex"01 41" ++ hex"02 4142", + hex"fe" ++ ByteVector.fill(0xfe)(0x41), + hex"ffff00" ++ ByteVector.fill(0xff)(0x41), + hex"ff0001" ++ ByteVector.fill(0x100)(0x41), + hex"ff ffff ffff 0000" ++ ByteVector.fill(0x0000ffff)(0x41), + hex"ff ffff 0000 0100" ++ ByteVector.fill(0x00010000)(0x41) + ) + + val packets = Vector( + MultiPacketEx(Vector(ByteVector.empty)), + MultiPacketEx(Vector(hex"41")), + MultiPacketEx(Vector(hex"41", hex"4142")), + MultiPacketEx(Vector(ByteVector.fill(0xfe)(0x41))), + MultiPacketEx(Vector(ByteVector.fill(0xff)(0x41))), + MultiPacketEx(Vector(ByteVector.fill(0x100)(0x41))), + MultiPacketEx(Vector(ByteVector.fill(0x0000ffff)(0x41))), + MultiPacketEx(Vector(ByteVector.fill(0x00010000)(0x41))) + ) + + "decode" in { + Fragment.foreach(strings.indices) { i => + "test "+i ! { MultiPacketEx.decode(strings{i}.bits).require.value mustEqual packets{i} } + } + } + + "encode" in { + Fragment.foreach(packets.indices) { i => + "test "+i ! { MultiPacketEx.encode(packets{i}).require.toByteVector mustEqual strings{i} } + } + } +} diff --git a/common/src/test/scala/control/RelatedATest.scala b/common/src/test/scala/control/RelatedATest.scala new file mode 100644 index 000000000..6e28c9559 --- /dev/null +++ b/common/src/test/scala/control/RelatedATest.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class RelatedATest extends Specification { + val string0 = hex"00 11 01 04" + + "decode (0)" in { + PacketCoding.DecodePacket(string0).require match { + case RelatedA0(slot) => + slot mustEqual 260 + case _ => + ko + } + } + + "encode (0)" in { + val pkt = RelatedA0(260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string0 + } +} diff --git a/common/src/test/scala/control/RelatedBTest.scala b/common/src/test/scala/control/RelatedBTest.scala new file mode 100644 index 000000000..f9dbe56e4 --- /dev/null +++ b/common/src/test/scala/control/RelatedBTest.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class RelatedBTest extends Specification { + val string0 = hex"00 15 01 04" + + "decode (0)" in { + PacketCoding.DecodePacket(string0).require match { + case RelatedB0(slot) => + slot mustEqual 260 + case _ => + ko + } + } + + "encode (0)" in { + val pkt = RelatedB0(260) + val msg = PacketCoding.EncodePacket(pkt).require.toByteVector + msg mustEqual string0 + } +} diff --git a/common/src/test/scala/control/SlottedMetaPacketTest.scala b/common/src/test/scala/control/SlottedMetaPacketTest.scala new file mode 100644 index 000000000..7f5569ecf --- /dev/null +++ b/common/src/test/scala/control/SlottedMetaPacketTest.scala @@ -0,0 +1,76 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import org.specs2.specification.core.Fragment +import scodec.bits._ +import scodec.codecs.uint16 + +class SlottedMetaPacketTest extends Specification { + val string = hex"00 09 00 00 00194302484C36563130433F" ++ + hex"4C6835316369774A0000000018FABE0C" ++ + hex"00000000000000000000000001000000" ++ + hex"020000006B7BD8288C6469666671756F" ++ + hex"7469656E740000000000440597570065" ++ + hex"006C0063006F006D006500200074006F" ++ + hex"00200050006C0061006E006500740053" ++ + hex"0069006400650021002000018667656D" ++ + hex"696E690100040001459E2540377540" + + def createMetaPacket(slot : Int, subslot : Int, rest : ByteVector) = hex"00" ++ + ControlPacketOpcode.codec.encode( + ControlPacketOpcode(ControlPacketOpcode.SlottedMetaPacket0.id + slot) + ).require.toByteVector ++ uint16.encode(subslot).require.toByteVector ++ rest + + "decode as the base slot and subslot" in { + PacketCoding.DecodePacket(string).require match { + case SlottedMetaPacket(slot, subslot, rest) => + slot mustEqual 0 + subslot mustEqual 0 + rest mustEqual string.drop(4) + case _ => + ko + } + } + + "decode as an arbitrary slot and subslot" in { + val maxSlots = ControlPacketOpcode.SlottedMetaPacket7.id - ControlPacketOpcode.SlottedMetaPacket0.id + + // create all possible SlottedMetaPackets + Fragment.foreach(0 to maxSlots) { i => + "slot " + i ! { + val subslot = 12323 + val pkt = createMetaPacket(i, subslot, ByteVector.empty) + + PacketCoding.DecodePacket(pkt).require match { + case SlottedMetaPacket(slot, subslotDecoded, rest) => + + // XXX: there isn't a simple solution to Slot0 and Slot4 be aliases of each other structurally + // This is probably best left to higher layers + //slot mustEqual i % 4 // this is seen at 0x00A3FBFA + slot mustEqual i + subslotDecoded mustEqual subslot + rest mustEqual ByteVector.empty // empty in this case + case _ => + ko + } + } + } + } + + "encode" in { + val encoded = PacketCoding.EncodePacket(SlottedMetaPacket(0, 0x1000, ByteVector.empty)).require + val encoded2 = PacketCoding.EncodePacket(SlottedMetaPacket(3, 0xffff, hex"414243")).require + val encoded3 = PacketCoding.EncodePacket(SlottedMetaPacket(7, 0, hex"00")).require + + encoded.toByteVector mustEqual createMetaPacket(0, 0x1000, ByteVector.empty) + encoded2.toByteVector mustEqual createMetaPacket(3, 0xffff, hex"414243") + encoded3.toByteVector mustEqual createMetaPacket(7, 0, hex"00") + + PacketCoding.EncodePacket(SlottedMetaPacket(8, 0, hex"00")).require must throwA[AssertionError] + PacketCoding.EncodePacket(SlottedMetaPacket(-1, 0, hex"00")).require must throwA[AssertionError] + PacketCoding.EncodePacket(SlottedMetaPacket(0, 0x10000, hex"00")).require must throwA[IllegalArgumentException] + } +} diff --git a/common/src/test/scala/control/TeardownConnectionTest.scala b/common/src/test/scala/control/TeardownConnectionTest.scala new file mode 100644 index 000000000..dc62f354a --- /dev/null +++ b/common/src/test/scala/control/TeardownConnectionTest.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2017 PSForever +package control + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.control._ +import scodec.bits._ + +class TeardownConnectionTest extends Specification { + val string = hex"00 05 02 4F 57 17 00 06" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case TeardownConnection(nonce) => + nonce mustEqual 391597826 + case _ => + ko + } + } + + "encode" in { + val encoded = PacketCoding.EncodePacket(TeardownConnection(391597826)).require + + encoded.toByteVector mustEqual string + } +} diff --git a/pslogin/src/main/scala/CryptoSessionActor.scala b/pslogin/src/main/scala/CryptoSessionActor.scala index 4af704b9d..db3f98746 100644 --- a/pslogin/src/main/scala/CryptoSessionActor.scala +++ b/pslogin/src/main/scala/CryptoSessionActor.scala @@ -1,17 +1,13 @@ // Copyright (c) 2017 PSForever -import java.net.{InetAddress, InetSocketAddress} - -import akka.actor.{Actor, ActorLogging, ActorRef, DiagnosticActorLogging, Identify, MDCContextAware} -import net.psforever.crypto.CryptoInterface.{CryptoState, CryptoStateWithMAC} +import akka.actor.{Actor, ActorRef, MDCContextAware} +import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC import net.psforever.crypto.CryptoInterface import net.psforever.packet._ import scodec.Attempt.{Failure, Successful} import scodec.bits._ -import scodec.{Attempt, Codec, Err} -import scodec.codecs.{bytes, uint16L, uint8L} import java.security.SecureRandom -import net.psforever.packet.control.{ClientStart, ServerStart, TeardownConnection} +import net.psforever.packet.control._ import net.psforever.packet.crypto._ import net.psforever.packet.game.PingMsg import org.log4s.MDC @@ -55,18 +51,19 @@ class CryptoSessionActor extends Actor with MDCContextAware { def receive = Initializing def Initializing : Receive = { - case HelloFriend(sessionId, right) => + case HelloFriend(sharedSessionId, pipe) => import MDCContextAware.Implicits._ - this.sessionId = sessionId + this.sessionId = sharedSessionId leftRef = sender() - rightRef = right.asInstanceOf[ActorRef] - - // who ever we send to has to send something back to us - rightRef !> HelloFriend(sessionId, self) - + if(pipe.hasNext) { + rightRef = pipe.next // who ever we send to has to send something back to us + rightRef !> HelloFriend(sessionId, pipe) + } else { + rightRef = sender() + } log.trace(s"Left sender ${leftRef.path.name}") - context.become(NewClient) + case default => log.error("Unknown message " + default) context.stop(self) @@ -85,10 +82,10 @@ class CryptoSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, serverNonce))) context.become(CryptoExchange) - case default => - log.error(s"Unexpected packet type ${p} in state NewClient") + case _ => + log.error(s"Unexpected packet type $p in state NewClient") } - case Failure(e) => + case Failure(_) => // There is a special case where no crypto is being used. // The only packet coming through looks like PingMsg. This is a hardcoded // feature of the client @ 0x005FD618 @@ -98,56 +95,53 @@ class CryptoSessionActor extends Actor with MDCContextAware { case ping @ PingMsg(_, _) => // reflect the packet back to the sender sendResponse(ping) - case default => log.error(s"Unexpected non-crypto packet type ${packet} in state NewClient") + case _ => + log.error(s"Unexpected non-crypto packet type $packet in state NewClient") } case Failure(e) => log.error("Could not decode packet: " + e + s" in state NewClient") } } - case default => log.error(s"Invalid message '$default' received in state NewClient") + case default => + log.error(s"Invalid message '$default' received in state NewClient") } def CryptoExchange : Receive = { case RawPacket(msg) => PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match { - case Failure(e) => log.error("Could not decode packet in state CryptoExchange: " + e) - case Successful(p) => - log.trace("NewClient -> CryptoExchange") + case Failure(e) => + log.error("Could not decode packet in state CryptoExchange: " + e) - p match { + case Successful(pkt) => + log.trace("NewClient -> CryptoExchange") + pkt match { case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) => cryptoDHState = Some(new CryptoInterface.CryptoDHState()) - val dh = cryptoDHState.get - // initialize our crypto state from the client's P and G dh.start(p, g) - // save the client challenge clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge) - // save the packet we got for a MAC check later. drop the first 3 bytes serverMACBuffer ++= msg.drop(3) - val serverTime = System.currentTimeMillis() / 1000L val randomChallenge = getRandBytes(0xc) - // store the complete server challenge for later serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge) - val packet = PacketCoding.CreateCryptoPacket(seq, - ServerChallengeXchg(serverTime, randomChallenge, dh.getPublicKey)) - + ServerChallengeXchg(serverTime, randomChallenge, dh.getPublicKey) + ) val sentPacket = sendResponse(packet) - // save the sent packet a MAC check serverMACBuffer ++= sentPacket.drop(3) - context.become(CryptoSetupFinishing) - case default => log.error(s"Unexpected packet type $p in state CryptoExchange") + + case _ => + log.error(s"Unexpected packet type $pkt in state CryptoExchange") } } - case default => log.error(s"Invalid message '$default' received in state CryptoExchange") + case default => + log.error(s"Invalid message '$default' received in state CryptoExchange") } def CryptoSetupFinishing : Receive = { @@ -188,11 +182,10 @@ class CryptoSessionActor extends Actor with MDCContextAware { ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01", 0xc) - val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret, - ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01", - 0xc) - - //println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult)) +// val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret, +// ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01", +// 0xc) +// println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult)) val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++ hex"00000000" ++ clientChallenge ++ hex"00000000" @@ -239,27 +232,32 @@ class CryptoSessionActor extends Actor with MDCContextAware { } def Established : Receive = { + //same as having received ad hoc hexadecimal case RawPacket(msg) => if(sender() == rightRef) { val packet = PacketCoding.encryptPacket(cryptoState.get, 0, msg).require sendResponse(packet) - } else { + } else { //from network-side PacketCoding.UnmarshalPacket(msg) match { case Successful(p) => p match { - case encPacket @ EncryptedPacket(seq, _) => - PacketCoding.decryptPacket(cryptoState.get, encPacket) match { + case encPacket @ EncryptedPacket(_/*seq*/, _) => + PacketCoding.decryptPacketData(cryptoState.get, encPacket) match { case Successful(packet) => - self !> packet + MDC("sessionId") = sessionId.toString + rightRef !> RawPacket(packet) case Failure(e) => log.error("Failed to decode encrypted packet: " + e) } - case default => failWithError(s"Unexpected packet type $default in state Established") + case default => + failWithError(s"Unexpected packet type $default in state Established") } - case Failure(e) => log.error("Could not decode raw packet: " + e) + case Failure(e) => + log.error("Could not decode raw packet: " + e) } } + //message to self? case api : CryptoSessionAPI => api match { case DropCryptoSession() => @@ -268,17 +266,12 @@ class CryptoSessionActor extends Actor with MDCContextAware { PacketCoding.CreateControlPacket(TeardownConnection(clientNonce)) ) } - case ctrl @ ControlPacket(_, _) => - val from = sender() - - handleEstablishedPacket(from, ctrl) - case game @ GamePacket(_, _, _) => - val from = sender() - - handleEstablishedPacket(from, game) + //echo the session router? isn't that normally the leftRef? case sessionAPI : SessionRouterAPI => leftRef !> sessionAPI - case default => failWithError(s"Invalid message '$default' received in state Established") + //error + case default => + failWithError(s"Invalid message '$default' received in state Established") } def failWithError(error : String) = { @@ -311,32 +304,35 @@ class CryptoSessionActor extends Actor with MDCContextAware { clientChallengeResult = ByteVector.empty } - def handleEstablishedPacket(from : ActorRef, cont : PlanetSidePacketContainer) = { - // we are processing a packet we decrypted - if(from == self) { + def handleEstablishedPacket(from : ActorRef, cont : PlanetSidePacketContainer) : Unit = { + //we are processing a packet that we decrypted + if(from == self) { //to WSA, LSA, etc. rightRef !> cont - } else if(from == rightRef) { // processing a completed packet from the right. encrypt - val packet = PacketCoding.encryptPacket(cryptoState.get, cont).require - sendResponse(packet) + } else if(from == rightRef) { //processing a completed packet from the right; to network-side + PacketCoding.getPacketDataForEncryption(cont) match { + case Successful((seq, data)) => + val packet = PacketCoding.encryptPacket(cryptoState.get, seq, data).require + sendResponse(packet) + case Failure(ex) => + log.error(s"$ex") + } } else { - log.error(s"Invalid sender when handling a message in Established ${from}") + log.error(s"Invalid sender when handling a message in Established $from") } } def sendResponse(cont : PlanetSidePacketContainer) : ByteVector = { log.trace("CRYPTO SEND: " + cont) val pkt = PacketCoding.MarshalPacket(cont) - pkt match { - case Failure(e) => + case Failure(_) => log.error(s"Failed to marshal packet ${cont.getClass.getName} when sending response") ByteVector.empty + case Successful(v) => val bytes = v.toByteVector - MDC("sessionId") = sessionId.toString leftRef !> ResponsePacket(bytes) - bytes } } @@ -344,17 +340,15 @@ class CryptoSessionActor extends Actor with MDCContextAware { def sendResponse(pkt : PlanetSideGamePacket) : ByteVector = { log.trace("CRYPTO SEND GAME: " + pkt) val pktEncoded = PacketCoding.EncodePacket(pkt) - pktEncoded match { - case Failure(e) => + case Failure(_) => log.error(s"Failed to encode packet ${pkt.getClass.getName} when sending response") ByteVector.empty + case Successful(v) => val bytes = v.toByteVector - MDC("sessionId") = sessionId.toString leftRef !> ResponsePacket(bytes) - bytes } } diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index 426086cf5..9010e0437 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -1,23 +1,19 @@ // Copyright (c) 2017 PSForever -import java.net.{InetAddress, InetSocketAddress} +import java.net.InetSocketAddress import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import net.psforever.packet.{PlanetSideGamePacket, _} import net.psforever.packet.control._ import net.psforever.packet.game._ 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 class LoginSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger @@ -29,7 +25,7 @@ class LoginSessionActor extends Actor with MDCContextAware { var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender - var updateServerListTask : Cancellable = null + var updateServerListTask : Cancellable = LoginSessionActor.DefaultCancellable override def postStop() = { if(updateServerListTask != null) @@ -39,12 +35,17 @@ class LoginSessionActor extends Actor with MDCContextAware { def receive = Initializing def Initializing : Receive = { - case HelloFriend(sessionId, right) => - this.sessionId = sessionId + case HelloFriend(aSessionId, pipe) => + this.sessionId = aSessionId leftRef = sender() - rightRef = right.asInstanceOf[ActorRef] - + if(pipe.hasNext) { + rightRef = pipe.next + rightRef !> HelloFriend(aSessionId, pipe) + } else { + rightRef = sender() + } context.become(Started) + case _ => log.error("Unknown message") context.stop(self) @@ -91,7 +92,7 @@ class LoginSessionActor extends Actor with MDCContextAware { /// 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}") + log.trace(s"SYNC: $sync") val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick, @@ -131,10 +132,9 @@ class LoginSessionActor extends Actor with MDCContextAware { 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) => { + case Some(resultSet) => val row : RowData = resultSet.head row(0) - } case None => -1 } ) @@ -161,12 +161,12 @@ class LoginSessionActor extends Actor with MDCContextAware { // 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}" + val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" if(token.isDefined) - log.info(s"New login UN:$username Token:${token.get}. ${clientVersion}") + log.info(s"New login UN:$username Token:${token.get}. $clientVersion") else - log.info(s"New login UN:$username PW:$password. ${clientVersion}") + log.info(s"New login UN:$username PW:$password. $clientVersion") // This is temporary until a schema has been developed //val loginSucceeded = accountLookup(username, password.getOrElse(token.get)) @@ -187,16 +187,16 @@ class LoginSessionActor extends Actor with MDCContextAware { val response = LoginRespMessage(newToken, LoginError.BadUsernameOrPassword, StationError.AccountActive, StationSubscriptionStatus.Active, 685276011, username, 10001) - log.info(s"Failed login to account ${username}") + 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}'") + log.info(s"Connect to world request for '$name'") val response = ConnectToWorldMessage(serverName, serverAddress.getHostString, serverAddress.getPort) sendResponse(PacketCoding.CreateGamePacket(0, response)) sendResponse(DropSession(sessionId, "user transferring to world")) - case default => log.debug(s"Unhandled GamePacket ${pkt}") + case default => log.debug(s"Unhandled GamePacket $pkt") } def updateServerList() = { @@ -228,3 +228,10 @@ class LoginSessionActor extends Actor with MDCContextAware { rightRef !> RawPacket(pkt) } } + +object LoginSessionActor { + final val DefaultCancellable = new Cancellable() { + def isCancelled : Boolean = true + def cancel : Boolean = true + } +} diff --git a/pslogin/src/main/scala/PacketCodingActor.scala b/pslogin/src/main/scala/PacketCodingActor.scala new file mode 100644 index 000000000..7fc39476e --- /dev/null +++ b/pslogin/src/main/scala/PacketCodingActor.scala @@ -0,0 +1,213 @@ +// Copyright (c) 2017 PSForever +import akka.actor.{Actor, ActorRef, MDCContextAware} +import net.psforever.packet._ +import scodec.Attempt.{Failure, Successful} +import scodec.bits._ +import org.log4s.MDC +import MDCContextAware.Implicits._ +import net.psforever.packet.control.{HandleGamePacket, SlottedMetaPacket} + +/** + * In between the network side and the higher functioning side of the simulation: + * accept packets and transform them into a sequence of data (encoding), and + * accept a sequence of data and transform it into s packet (decoding).
+ *
+ * Following the standardization of the `SessionRouter` pipeline, the throughput of this `Actor` has directionality. + * The "network," where the encoded data comes and goes, is assumed to be `leftRef`. + * The "simulation", where the decoded packets come and go, is assumed to be `rightRef`. + * `rightRef` can accept a sequence that looks like encoded data but it will merely pass out the same sequence. + * Likewise, `leftRef` accepts decoded packets but merely ejects the same packets without doing any work on them. + * The former functionality is anticipated. + * The latter functionality is deprecated.
+ *
+ * Encoded data leaving the `Actor` (`leftRef`) is limited by an upper bound capacity. + * Sequences can not be larger than that bound or else they will be dropped. + * This maximum transmission unit (MTU) is used to divide the encoded sequence into chunks of encoded data, + * re-packaged into nested `ControlPacket` units, and each unit encoded. + * The outer packaging is numerically consistent with a `subslot` that starts counting once the simulation starts. + * The client is very specific about the `subslot` number and will reject out-of-order packets. + * It resets to 0 each time this `Actor` starts up and the client reflects this functionality. + */ +class PacketCodingActor extends Actor with MDCContextAware { + private var sessionId : Long = 0 + private var subslot : Int = 0 + private var leftRef : ActorRef = ActorRef.noSender + private var rightRef : ActorRef = ActorRef.noSender + private[this] val log = org.log4s.getLogger + + override def postStop() = { + subslot = 0 //in case this `Actor` restarts + super.postStop() + } + + def receive = Initializing + + def Initializing : Receive = { + case HelloFriend(sharedSessionId, pipe) => + import MDCContextAware.Implicits._ + this.sessionId = sharedSessionId + leftRef = sender() + if(pipe.hasNext) { + rightRef = pipe.next + rightRef !> HelloFriend(sessionId, pipe) + } + else { + rightRef = sender() + } + log.trace(s"Left sender ${leftRef.path.name}") + context.become(Established) + + case default => + log.error("Unknown message " + default) + context.stop(self) + } + + def Established : Receive = { + case RawPacket(msg) => + if(sender == rightRef) { //from LSA, WSA, etc., to network - encode + mtuLimit(msg) + } + else {//from network, to LSA, WSA, etc. - decode + PacketCoding.unmarshalPayload(0, msg) match { //TODO is it safe for this to always be 0? + case Successful(packet) => + sendResponseRight(packet) + case Failure(ex) => + log.info(s"Failed to marshal a packet: $ex") + } + } + //known elevated packet type + case ctrl @ ControlPacket(_, packet) => + if(sender == rightRef) { //from LSA, WSA, to network - encode + PacketCoding.EncodePacket(packet) match { + case Successful(data) => + mtuLimit(data.toByteVector) + case Failure(ex) => + log.error(s"Failed to encode a ControlPacket: $ex") + } + } + else { //deprecated; ControlPackets should not be coming from this direction + log.warn(s"DEPRECATED CONTROL PACKET SEND: $ctrl") + MDC("sessionId") = sessionId.toString + sendResponseRight(ctrl) + } + //known elevated packet type + case game @ GamePacket(_, _, packet) => + if(sender == rightRef) { //from LSA, WSA, etc., to network - encode + PacketCoding.EncodePacket(packet) match { + case Successful(data) => + mtuLimit(data.toByteVector) + case Failure(ex) => + log.error(s"Failed to encode a GamePacket: $ex") + } + } + else { //deprecated; GamePackets should not be coming from this direction + log.warn(s"DEPRECATED GAME PACKET SEND: $game") + MDC("sessionId") = sessionId.toString + sendResponseRight(game) + } + //etc + case msg => + log.trace(s"PACKET SEND, LEFT: $msg") + if(sender == rightRef) { + MDC("sessionId") = sessionId.toString + leftRef !> msg + } + else { + MDC("sessionId") = sessionId.toString + rightRef !> msg + } +// case default => +// failWithError(s"Invalid message '$default' received in state Established") + } + + def resetState() : Unit = { + context.become(receive) + } + + /** + * Retrieve the current subslot number. + * Increment the `subslot` for the next time it is needed. + * @return a 16u number starting at 0 + */ + def Subslot : Int = { + if(subslot == 65536) { //TODO what is the actual wrap number? + subslot = 0 + subslot + } else { + val curr = subslot + subslot += 1 + curr + } + } + + /** + * Check that an outbound packet is not too big to get stuck by the MTU. + * If it is larger than the MTU, divide it up and re-package the sections. + * Otherwise, send the data out like normal. + * @param msg the encoded packet data + */ + def mtuLimit(msg : ByteVector) : Unit = { + if(msg.length > PacketCodingActor.MTU_LIMIT_BYTES) { + handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(msg))) + } + else { + sendResponseLeft(msg) + } + } + + /** + * Transform a `ControlPacket` into `ByteVector` data for splitting. + * @param cont the original `ControlPacket` + */ + def handleSplitPacket(cont : ControlPacket) : Unit = { + PacketCoding.getPacketDataForEncryption(cont) match { + case Successful((_, data)) => + handleSplitPacket(data) + case Failure(ex) => + log.error(s"$ex") + } + } + + /** + * Accept `ByteVector` data, representing a `ControlPacket`, and split it into chunks. + * The chunks should not be blocked by the MTU. + * Send each chunk (towards the network) as it is converted. + * @param data `ByteVector` data to be split + */ + def handleSplitPacket(data : ByteVector) : Unit = { + val lim = PacketCodingActor.MTU_LIMIT_BYTES - 4 //4 bytes is the base size of SlottedMetaPacket + data.grouped(lim).foreach(bvec => { + val pkt = PacketCoding.CreateControlPacket(SlottedMetaPacket(4, Subslot, bvec)) + PacketCoding.EncodePacket(pkt.packet) match { + case Successful(bdata) => + sendResponseLeft(bdata.toByteVector) + case f @ Failure(_) => + log.error(s"$f") + } + }) + } + + /** + * Encoded sequence of data going towards the network. + * @param cont the data + */ + def sendResponseLeft(cont : ByteVector) : Unit = { + log.trace("PACKET SEND, LEFT: " + cont) + MDC("sessionId") = sessionId.toString + leftRef !> RawPacket(cont) + } + + /** + * Decoded packet going towards the simulation. + * @param cont the packet + */ + def sendResponseRight(cont : PlanetSidePacketContainer) : Unit = { + log.trace("PACKET SEND, RIGHT: " + cont) + MDC("sessionId") = sessionId.toString + rightRef !> cont + } +} + +object PacketCodingActor { + final val MTU_LIMIT_BYTES : Int = 467 +} diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index fefc32c80..fab1d3e70 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -3,7 +3,7 @@ import java.net.InetAddress import java.io.File import java.util.Locale -import akka.actor.{ActorSystem, Props} +import akka.actor.{ActorRef, ActorSystem, Props} import akka.routing.RandomPool import ch.qos.logback.classic.LoggerContext import ch.qos.logback.classic.joran.JoranConfigurator @@ -24,17 +24,16 @@ import scala.collection.JavaConverters._ import scala.concurrent.Await import scala.concurrent.duration._ - object PsLogin { private val logger = org.log4s.getLogger var args : Array[String] = Array() var config : java.util.Map[String,Object] = null - implicit var system : akka.actor.ActorSystem = null - var loginRouter : akka.actor.Props = null - var worldRouter : akka.actor.Props = null - var loginListener : akka.actor.ActorRef = null - var worldListener : akka.actor.ActorRef = null + implicit var system : ActorSystem = null + var loginRouter : Props = Props.empty + var worldRouter : Props = Props.empty + var loginListener : ActorRef = ActorRef.noSender + var worldListener : ActorRef = ActorRef.noSender def banner() : Unit = { println(ansi().fgBright(BLUE).a(""" ___ ________""")) @@ -178,10 +177,12 @@ object PsLogin { */ val loginTemplate = List( SessionPipeline("crypto-session-", Props[CryptoSessionActor]), + SessionPipeline("packet-session-", Props[PacketCodingActor]), SessionPipeline("login-session-", Props[LoginSessionActor]) ) val worldTemplate = List( SessionPipeline("crypto-session-", Props[CryptoSessionActor]), + SessionPipeline("packet-session-", Props[PacketCodingActor]), SessionPipeline("world-session-", Props[WorldSessionActor]) ) diff --git a/pslogin/src/main/scala/Session.scala b/pslogin/src/main/scala/Session.scala index 3b74b33e4..b0fbd6438 100644 --- a/pslogin/src/main/scala/Session.scala +++ b/pslogin/src/main/scala/Session.scala @@ -35,7 +35,10 @@ class Session(val sessionId : Long, a } - pipeline.head ! HelloFriend(sessionId, pipeline.tail.head) + val pipelineIter = pipeline.iterator + if(pipelineIter.hasNext) { + pipelineIter.next ! HelloFriend(sessionId, pipelineIter) + } // statistics var bytesSent : Long = 0 diff --git a/pslogin/src/main/scala/UdpListener.scala b/pslogin/src/main/scala/UdpListener.scala index d34f14ee3..dffc15f1b 100644 --- a/pslogin/src/main/scala/UdpListener.scala +++ b/pslogin/src/main/scala/UdpListener.scala @@ -11,7 +11,7 @@ import akka.util.ByteString final case class ReceivedPacket(msg : ByteVector, from : InetSocketAddress) final case class SendPacket(msg : ByteVector, to : InetSocketAddress) final case class Hello() -final case class HelloFriend(sessionId : Long, next: ActorRef) +final case class HelloFriend(sessionId : Long, next: Iterator[ActorRef]) class UdpListener(nextActorProps : Props, nextActorName : String, diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 52094defe..7423c32e9 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -60,16 +60,20 @@ class WorldSessionActor extends Actor with MDCContextAware { def receive = Initializing def Initializing : Receive = { - case HelloFriend(inSessionId, right) => + case HelloFriend(inSessionId, pipe) => this.sessionId = inSessionId leftRef = sender() - rightRef = right.asInstanceOf[ActorRef] - + if(pipe.hasNext) { + rightRef = pipe.next + rightRef !> HelloFriend(sessionId, pipe) + } else { + rightRef = sender() + } + context.become(Started) ServiceManager.serviceManager ! Lookup("avatar") ServiceManager.serviceManager ! Lookup("accessor1") ServiceManager.serviceManager ! Lookup("taskResolver") - context.become(Started) case _ => log.error("Unknown message") context.stop(self) @@ -537,8 +541,18 @@ class WorldSessionActor extends Actor with MDCContextAware { handlePkt(v) } } + + case RelatedA0(subslot) => + log.error(s"Client not ready for last control packet with subslot $subslot; potential system disarray") + + case RelatedB0(subslot) => + log.trace(s"Good control packet received $subslot") + + case TeardownConnection(_) => + log.info("Good bye") + case default => - log.debug(s"Unhandled ControlPacket $default") + log.warn(s"Unhandled ControlPacket $default") } }