From c117f051654e4bf8ef7b457228d2ab484be80d00 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 24 May 2017 09:58:15 -0400 Subject: [PATCH] tidied up the first part of PacketCoding; made certain basic client-server communication still works --- .../net/psforever/packet/PacketCoding.scala | 393 ++++++++++-------- 1 file changed, 221 insertions(+), 172 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PacketCoding.scala b/common/src/main/scala/net/psforever/packet/PacketCoding.scala index 3a12b561..2074cbaa 100644 --- a/common/src/main/scala/net/psforever/packet/PacketCoding.scala +++ b/common/src/main/scala/net/psforever/packet/PacketCoding.scala @@ -2,11 +2,10 @@ package net.psforever.packet import net.psforever.crypto.CryptoInterface -import net.psforever.crypto.CryptoInterface._ import scodec.Attempt.{Successful, Failure} import scodec.bits._ -import scodec.{DecodeResult, Err, Attempt, Codec} -import scodec.codecs.{uint16L, uint8L, uint4L, bytes} +import scodec.{Err, Attempt, Codec} +import scodec.codecs.{uint16L, uint8L, bytes} /// Packet container base trait sealed trait PlanetSidePacketContainer @@ -30,144 +29,110 @@ 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 = 1 + /** + * Access to the `ControlPacket` constructor. + * @param packet a `PlanetSideControlPacket` + * @return a `ControlPacket` wrapper + */ + def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, 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. - * - * @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 + * Access to the `CryptoPacket` constructor. + * @param sequence na + * @param packet a `PlanetSideCryptoPacket` + * @return a `CryptoPacket` wrapper */ - def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { - // check for a minimum length - 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 { - // drop the first byte as control packets dont need it - case 0x00 => unmarshalControlPacket(msg.drop(1)) - case _ => unmarshalFlaggedPacket(msg, cryptoState) // returns either EncryptedPacket or CryptoPacket - } - } + def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet) /** - * 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 + * Access to the `GamePacket` constructor. + * @param sequence na + * @param packet a `PlanetSideGamePacket` + * @return a `GamePacket` wrapper */ - def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = { - UnmarshalPacket(msg, CryptoPacketOpcode.Ignore) - } + def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet) + +/* Marshalling and Encoding. */ /** - * 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 + * Transforms a packet into `BitVector` representations of its component data and then re-assemble 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 */ - def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = { - // check for a minimum length - 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 { - // drop the first byte as control packets dont need it - case 0x00 => DecodeControlPacket(msg.drop(1)) - case _ => DecodeGamePacket(msg) - } - } - def MarshalPacket(packet : PlanetSidePacketContainer) : Attempt[BitVector] = { - var flagsEncoded : BitVector = BitVector.empty - var seqEncoded : BitVector = BitVector.empty - var paddingEncoded : BitVector = BitVector.empty - var opcodeEncoded : BitVector = BitVector.empty - var payloadEncoded : BitVector = BitVector.empty - + var flagsEncoded : BitVector = BitVector.empty //flags before everything in packet + var seqEncoded : BitVector = BitVector.empty //control packets have a sequence number + var paddingEncoded : BitVector = BitVector.empty //encrypted packets need to be aligned in a certain way + var payloadEncoded : BitVector = BitVector.empty //the packet itself as bits and bytes var controlPacket = false var sequenceNum = 0 - - // packet flags - var hasFlags = true + //packet flags var secured = false var packetType = PacketType.Crypto packet match { - case GamePacket(opcode, seq, payload) => - secured = false + case GamePacket(_, seq, payload) => packetType = PacketType.Normal sequenceNum = seq - EncodePacket(payload) match { - case f @ Failure(e) => return f + case f @ Failure(_) => return f case Successful(p) => payloadEncoded = p } - case ControlPacket(opcode, payload) => + + case ControlPacket(_, payload) => controlPacket = true - EncodePacket(payload) match { - case f @ Failure(e) => return f + case f @ Failure(_) => return f case Successful(p) => payloadEncoded = p } + case CryptoPacket(seq, payload) => - secured = false packetType = PacketType.Crypto sequenceNum = seq - EncodePacket(payload) match { - case f @ Failure(e) => return f + case f @ Failure(_) => return f case Successful(p) => payloadEncoded = p } + case EncryptedPacket(seq, payload) => 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 + //encrypted packets need to be aligned to 4 bytes before encryption/decryption + //first byte are flags, second is the sequence, and third is the pad paddingEncoded = hex"00".bits payloadEncoded = payload.bits } - val flags = PlanetSidePacketFlags(packetType, secured = secured) - - // crypto packets DONT have flags + //crypto packets DON'T have flags if(!controlPacket) { + val flags = PlanetSidePacketFlags(packetType, secured = secured) 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) + Attempt.successful(flagsEncoded ++ seqEncoded ++ paddingEncoded ++ payloadEncoded) } + /** + * Overloaded method for transforming a control packet into its `BitVector` representation. + * @param packet the control packet to encode + * @return a `BitVector` translated from the packet's data + */ def EncodePacket(packet : PlanetSideControlPacket) : Attempt[BitVector] = { val opcode = packet.opcode - var opcodeEncoded = BitVector.empty - var payloadEncoded = BitVector.empty + var opcodeEncoded = 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 } + var payloadEncoded = BitVector.empty encodePacket(packet) match { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext)) case Successful(p) => payloadEncoded = p @@ -176,23 +141,33 @@ object PacketCoding { Attempt.Successful(hex"00".bits ++ opcodeEncoded ++ payloadEncoded) } + /** + * Overloaded method for transforming a crypto packet into its `BitVector` representation. + * @param packet the crypto packet to encode + * @return a `BitVector` translated from the packet's data + */ 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 + case s @ Successful(_) => s } } + /** + * Overloaded method for transforming a game packet into its `BitVector` representation. + * @param packet the game packet to encode + * @return a `BitVector` translated from the packet's data + */ def EncodePacket(packet : PlanetSideGamePacket) : Attempt[BitVector] = { val opcode = packet.opcode - var opcodeEncoded = BitVector.empty - var payloadEncoded = BitVector.empty + var opcodeEncoded = 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 } + var payloadEncoded = BitVector.empty encodePacket(packet) match { case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext)) case Successful(p) => payloadEncoded = p @@ -201,17 +176,62 @@ object PacketCoding { 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) - - ////////////////////////////////////////////////////////////////////////////// - + /** + * Calls the packet-specific encode function. + * Lowest encode call. + * @param packet the packet to encode + * @return a `BitVector` translated from the packet's data + */ private def encodePacket(packet : PlanetSidePacket) : Attempt[BitVector] = packet.encode - private def unmarshalFlaggedPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { - val decodedFlags = Codec.decode[PlanetSidePacketFlags](BitVector(msg)) +/* Unmarshalling and Decoding. */ + /** + * A lower bound on the packet size + */ + 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. + * @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] = { + 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 => unmarshalControlPacket(msg.drop(1)) //control packets dont need the first byte + case _ => unmarshalFlaggedPacket(msg, cryptoState) //either EncryptedPacket or CryptoPacket + } + } + + /** + * 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) + + /** + * Handle decoding for a packet that is not a control packet. + * It may just be encrypted or it may be involved in the encryption process itself. + * @param msg the packet + * @param cryptoState the current state of the connection's crypto + * @return a `PlanetSidePacketContainer` + */ + private def unmarshalFlaggedPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { + //get the flags + val decodedFlags = Codec.decode[PlanetSidePacketFlags](BitVector(msg)) decodedFlags match { case Failure(e) => return Attempt.failure(Err("Failed to parse packet flags: " + e.message)) @@ -219,112 +239,76 @@ object PacketCoding { } val flags = decodedFlags.require.value - val rest = decodedFlags.require.remainder val packetType = flags.packetType - - // perform a quick reject of weird packet types packetType match { - case PacketType.Crypto => ; - case PacketType.Normal => ; - case default => + case PacketType.Normal => + if(!flags.secured) { //support normal packets only if they are encrypted + return Attempt.failure(Err("Unsupported packet type: normal packets must be encryped")) + } + + case PacketType.Crypto => + if(flags.secured) { //support crypto packets only if they are not encrypted + return Attempt.failure(Err("Unsupported packet type: crypto packets must be unencrypted")) + } + + case _ => return Attempt.failure(Err("Unsupported packet type: " + flags.packetType.toString)) } - // we only support normal packets if they are encrypted - if(packetType == PacketType.Normal && !flags.secured) - return Attempt.failure(Err("Unsupported packet type: normal packets must be encryped")) - - // we only support crypto packets if they are not encrypted - if(packetType == PacketType.Crypto && flags.secured) - return Attempt.failure(Err("Unsupported packet type: crypto packets must be unencrypted")) - - // all packets have a two byte sequence ID - // TODO: make this a codec for reuse - val decodedSeq = uint16L.decode(rest) - + //all packets have a two byte sequence ID + val decodedSeq = uint16L.decode(decodedFlags.require.remainder) //TODO: make this a codec for reuse decodedSeq match { case Failure(e) => return Attempt.failure(Err("Failed to parse packet sequence number: " + e.message)) case _ => } - val sequence = decodedSeq.require.value - var payload = decodedSeq.require.remainder.toByteVector - - // encrypted packets must be 4-byte aligned - if(flags.secured) { - payload = payload.drop(1) - } + val payload = decodedSeq.require.remainder.toByteVector packetType match { case PacketType.Crypto => unmarshalCryptoPacket(cryptoState, sequence, payload) case PacketType.Normal => - unmarshalEncryptedPacket(sequence, payload) + unmarshalEncryptedPacket(sequence, payload.drop(1)) //payload is 4-byte aligned } } + /** + * Handle decoding for a control packet. + * @param msg the packet + * @return a `ControlPacket` + */ private def unmarshalControlPacket(msg : ByteVector) : Attempt[ControlPacket] = { - val packet = DecodeControlPacket(msg) - - packet match { - // just return the failure - case f @ Failure(e) => f + DecodeControlPacket(msg) match { + case f @ Failure(_) => f case Successful(p) => Attempt.successful(CreateControlPacket(p)) } } - def DecodeControlPacket(msg : ByteVector) : Attempt[PlanetSideControlPacket] = { - val opcode = ControlPacketOpcode.codec.decode(msg.bits) - - opcode match { - case Failure(e) => - Attempt.failure(Err("Failed to decode control packet's opcode: " + e.message)) - case Successful(op) => - val packet = ControlPacketOpcode.getPacketDecoder(op.value)(op.remainder) - - packet match { - case Failure(e) => - Attempt.failure(Err(f"Failed to parse control packet ${op.value}: " + e.messageWithContext)) - case Successful(p) => - Attempt.successful(p.value) - } - } - } - + /** + * Handle decoding for a game packet. + * @param sequence na + * @param msg the packet data + * @return a `GamePacket` + */ private def unmarshalGamePacket(sequence : Int, msg : ByteVector) : Attempt[GamePacket] = { - val packet = DecodeGamePacket(msg) - - packet match { - case f @ Failure(e) => f + DecodeGamePacket(msg) match { + case f @ Failure(_) => f case Successful(p) => Attempt.successful(CreateGamePacket(sequence, p)) } } - def DecodeGamePacket(msg : ByteVector) : Attempt[PlanetSideGamePacket] = { - val opcode = GamePacketOpcode.codec.decode(msg.bits) - - opcode match { - case Failure(e) => - return Attempt.failure(Err("Failed to decode game packet's opcode: " + e.message)) - case _ => - } - - val packet = GamePacketOpcode.getPacketDecoder(opcode.require.value)(opcode.require.remainder) - - packet match { - case Failure(e) => - Attempt.failure(Err(f"Failed to parse game packet 0x${opcode.require.value.id}%02x: " + e.messageWithContext)) - case Successful(p) => Attempt.successful(p.value) - } - } - + /** + * Handle decoding for a crypto packet. + * @param state the current state of the connection's crypto + * @param sequence na + * @param payload the packet data + * @return a `CryptoPacket` + */ private def unmarshalCryptoPacket(state : CryptoPacketOpcode.Type, sequence : Int, payload : ByteVector) : Attempt[CryptoPacket] = { - val packet = CryptoPacketOpcode.getPacketDecoder(state)(payload.bits) - - packet match { + CryptoPacketOpcode.getPacketDecoder(state)(payload.bits) match { case Successful(a) => Attempt.successful(CryptoPacket(sequence, a.value)) case Failure(e) => @@ -332,13 +316,78 @@ object PacketCoding { } } + /** + * Handle decoding for an encrypted packet. + * That is, it's already encrypted. + * Just repackage the data. + * @param sequence na + * @param payload the packet data + * @return an `EncryptedPacket` + */ private def unmarshalEncryptedPacket(sequence : Int, payload : ByteVector) : Attempt[EncryptedPacket] = { Attempt.successful(EncryptedPacket(sequence, payload)) } - /////////////////////////////////////////////////////////// - // Packet Crypto - /////////////////////////////////////////////////////////// + /** + * 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] = { + 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 + case _ => DecodeGamePacket(msg) + } + } + + /** + * Transform a `BitVector` into a control packet. + * @param msg the the raw data to decode + * @return a `PlanetSideControlPacket` + */ + def DecodeControlPacket(msg : ByteVector) : Attempt[PlanetSideControlPacket] = { + ControlPacketOpcode.codec.decode(msg.bits) match { + case Failure(e) => + Attempt.failure(Err("Failed to decode control packet's opcode: " + e.message)) + + case Successful(op) => + ControlPacketOpcode.getPacketDecoder(op.value)(op.remainder) match { + case Failure(e) => + Attempt.failure(Err(f"Failed to parse control packet ${op.value}: " + e.messageWithContext)) + + case Successful(p) => + Attempt.successful(p.value) + } + } + } + + /** + * Transform a `BitVector` into a game packet. + * @param msg the the raw data to decode + * @return a `PlanetSideGamePacket` + */ + def DecodeGamePacket(msg : ByteVector) : Attempt[PlanetSideGamePacket] = { + GamePacketOpcode.codec.decode(msg.bits) match { + case Failure(e) => + Attempt.failure(Err("Failed to decode game packet's opcode: " + e.message)) + + case Successful(opcode) => + GamePacketOpcode.getPacketDecoder(opcode.value)(opcode.remainder) match { + case Failure(e) => + Attempt.failure(Err(f"Failed to parse game packet 0x${opcode.value.id}%02x: " + e.messageWithContext)) + + case Successful(p) => + Attempt.successful(p.value) + } + } + } + +/* Encrypting and Decrypting. */ def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { // TODO XXX: this is bad. rework