Merge pull request #158 from Fate-JH/packet-split

Packet Splitting, Once Again
This commit is contained in:
Fate-JH 2017-09-01 08:37:47 -04:00 committed by GitHub
commit 808a40d080
23 changed files with 833 additions and 370 deletions

View file

@ -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)

View file

@ -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.<br>
* <br>
* 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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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
}
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]
}

View file

@ -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]
}

View file

@ -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]
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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} }
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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]
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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).<br>
* <br>
* 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.<br>
* <br>
* 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
}

View file

@ -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])
)

View file

@ -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

View file

@ -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,

View file

@ -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")
}
}