Merge pull request #194 from Fate-JH/pca

Packet Splitting, Bundling
This commit is contained in:
Fate-JH 2018-04-02 09:39:55 -04:00 committed by GitHub
commit fc3837268c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1156 additions and 268 deletions

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet
import net.psforever.packet.control.SlottedMetaPacket
import net.psforever.packet.control.{RelatedA, RelatedB, SlottedMetaPacket}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.codecs._
@ -74,15 +74,15 @@ object ControlPacketOpcode extends Enumeration {
// OPCODES 0x10-1e
case 0x10 => SlottedMetaPacket.decodeWithOpcode(SlottedMetaPacket7)
case 0x11 => control.RelatedA0.decode
case 0x12 => noDecoder(RelatedA1)
case 0x13 => noDecoder(RelatedA2)
case 0x14 => noDecoder(RelatedA3)
case 0x15 => control.RelatedB0.decode
case 0x16 => noDecoder(RelatedB1)
case 0x17 => noDecoder(RelatedB2)
case 0x11 => RelatedA.decodeWithOpcode(RelatedA0)
case 0x12 => RelatedA.decodeWithOpcode(RelatedA1)
case 0x13 => RelatedA.decodeWithOpcode(RelatedA2)
case 0x14 => RelatedA.decodeWithOpcode(RelatedA3)
case 0x15 => RelatedB.decodeWithOpcode(RelatedB0)
case 0x16 => RelatedB.decodeWithOpcode(RelatedB1)
case 0x17 => RelatedB.decodeWithOpcode(RelatedB2)
// 0x18
case 0x18 => noDecoder(RelatedB3)
case 0x18 => RelatedB.decodeWithOpcode(RelatedB3)
case 0x19 => control.MultiPacketEx.decode
case 0x1a => noDecoder(Unknown26)
case 0x1b => noDecoder(Unknown27)

View file

@ -0,0 +1,129 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.control
import net.psforever.packet.PlanetSidePacket
/**
* Message for holding a series of packets being moved through the system (server),
* eventually be bundled into a `MultiPacketEx` and dispatched to the client.
* Invalid packets are eliminated at the time of creation.
* At least one packet is necessary.
* @param packets a series of packets to be bundled together;
* this list is effectively immutable;
* the only way to access these packets is through pattern matching
*/
final case class MultiPacketBundle(private var packets : List[PlanetSidePacket]) {
MultiPacketBundle.collectValidPackets(packets) match {
case Nil =>
throw new IllegalArgumentException("can not create with zero packets")
case list =>
packets = list
}
def +(t : MultiPacketBundle) : MultiPacketBundle = t match {
case MultiPacketBundle(list) =>
MultiPacketBundle(packets ++ list)
case _ =>
MultiPacketBundle(packets)
}
}
object MultiPacketBundle {
/**
* Accept a series of packets of a specific supertype (`PlanetSidePacket`)
* and filter out subtypes that should be excluded.
* Show a generic disclaimer if any packets were filtered.
* Two of the four subclasses of `PlanetSidePacket` are accepted - `PlanetSideGamePacket` and `PlanetSideControlPacket`.
* @param packets a series of packets
* @return the accepted packets from the original group
*/
def collectValidPackets(packets : List[PlanetSidePacket]) : List[PlanetSidePacket] = {
import net.psforever.packet.{PlanetSideGamePacket, PlanetSideControlPacket}
val (good, bad) = packets.partition( {
case _ : PlanetSideGamePacket => true
case _ : PlanetSideControlPacket => true
case _ => false
})
if(bad.nonEmpty) {
org.log4s.getLogger("MultiPacketBundle")
.warn(s"attempted to include packet types that are not in the whitelist; ${bad.size} items have been excluded")
}
good
}
}
/**
* Accumulator for packets that will eventually be bundled and submitted for composing a `MultiPacketEx` packet.
*/
class MultiPacketCollector() {
private var bundle : List[PlanetSidePacket] = List.empty
def Add(t : PlanetSidePacket) : Unit = Add(List(t))
def Add(t : MultiPacketBundle) : Unit = t match {
case MultiPacketBundle(list) =>
Add(list)
}
def Add(t : List[PlanetSidePacket]) : Unit = {
if(t.nonEmpty) {
bundle = bundle ++ t
}
}
/**
* Retrieve the internal collection of packets.
* Reset the internal list of packets by clearing it.
* @return a loaded `MultiPacketBundle` object
*/
def Bundle : MultiPacketBundle = {
try {
val out = MultiPacketBundle(bundle)
bundle = List.empty
out
}
catch {
case _ : Exception => //catch and rethrow the exception
throw new RuntimeException("no packets")
}
}
/**
* A safer `Bundle` that consumes any` Exceptions` that might be thrown in the process of producing output.
* @see `Bundle`
* @return a loaded `MultiPacketBundle` object, or `None`
*/
def BundleOption : Option[MultiPacketBundle] = {
try {
Some(Bundle)
}
catch {
case _ : Exception =>
None
}
}
}
object MultiPacketCollector {
/**
* Overload constructor that accepts initial packets.
* @param bundle previously bundled packets
* @return a `MultiPacketCollector` object
*/
def apply(bundle : MultiPacketBundle) : MultiPacketCollector = {
val obj = new MultiPacketCollector()
obj.Add(bundle)
obj
}
/**
* Overload constructor that accepts initial packets.
* @param packets a series of packets
* @return a `MultiPacketCollector` object
*/
def apply(packets : List[PlanetSidePacket]) : MultiPacketCollector = {
val obj = new MultiPacketCollector()
obj.Add(packets)
obj
}
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.control
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket}
import scodec.Codec
import scodec.bits.BitVector
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 slot the type of `ResultA` packet;
* valid types are integers 0-3
* @param subslot identification of a control packet
*/
final case class RelatedA(slot : Int, subslot : Int) extends PlanetSideControlPacket {
type Packet = RelatedA
if(slot < 0 || slot > 3) {
throw new IllegalArgumentException(s"slot number is out of range - $slot")
}
def opcode = {
val base = ControlPacketOpcode.RelatedA0.id
ControlPacketOpcode(base + slot)
}
def encode = RelatedA.encode(this).map(vect => vect.drop(8))
}
object RelatedA extends Marshallable[RelatedA] {
implicit val codec : Codec[RelatedA] = (
("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedA0.id, a=>a) ) ::
("subslot" | uint16) // the slot is big endian. see 0x00A42F76
).as[RelatedA]
def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = {
decode(ControlPacketOpcode.codec.encode(slot).require ++ bits)
}
}

View file

@ -1,22 +0,0 @@
// 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,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.control
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket}
import scodec.Codec
import scodec.bits.BitVector
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 slot the type of `ResultB` packet;
* valid types are integers 0-3
* @param subslot identification of a control packet
*/
final case class RelatedB(slot : Int, subslot : Int) extends PlanetSideControlPacket {
type Packet = RelatedB
if(slot < 0 || slot > 3) {
throw new IllegalArgumentException(s"slot number is out of range - $slot")
}
def opcode = {
val base = ControlPacketOpcode.RelatedB0.id
ControlPacketOpcode(base + slot)
}
def encode = RelatedB.encode(this).map(vect => vect.drop(8))
}
object RelatedB extends Marshallable[RelatedB] {
implicit val codec : Codec[RelatedB] = (
("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedB0.id, a=>a) ) ::
("subslot" | uint16) // the slot is big endian. see 0x00A42F76
).as[RelatedB]
def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = {
decode(ControlPacketOpcode.codec.encode(slot).require ++ bits)
}
}

View file

@ -1,23 +0,0 @@
// 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,35 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.control
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket}
import scodec.Codec
import scodec.bits.BitVector
import scodec.codecs._
final case class SlottedMetaAck(slot : Int, subslot : Int)
extends PlanetSideControlPacket {
type Packet = SlottedMetaAck
assert(slot >= 0 && slot <= 7, s"Slot number ($slot) is out of range") //TODO 7 types of SlottedMeta, 4 types of ResultB?
def opcode = {
val base = ControlPacketOpcode.RelatedB0.id
ControlPacketOpcode(base + slot % 4)
}
// XXX: a nasty hack to ignore the "slot" field
// There is so much wrong with this it's not even funny. Why scodec, whyyyy...
// I've never had a library make me feel so stupid and smart at the same time
def encode = SlottedMetaAck.encode(this).map(vect => vect.drop(8))
}
object SlottedMetaAck extends Marshallable[SlottedMetaAck] {
implicit val codec : Codec[SlottedMetaAck] = (
("slot" | uint8L.xmap[Int](a => a - ControlPacketOpcode.RelatedB0.id, a=>a) ) ::
("subslot" | uint16)
).as[SlottedMetaAck]
def decodeWithOpcode(slot : ControlPacketOpcode.Value)(bits : BitVector) = {
decode(ControlPacketOpcode.codec.encode(slot).require ++ bits)
}
}

View file

@ -0,0 +1,183 @@
// Copyright (c) 2017 PSForever
package control
import org.specs2.mutable._
import net.psforever.packet.control.{ControlSync, MultiPacketBundle, MultiPacketCollector}
import net.psforever.packet.crypto.{ClientFinished, ServerFinished}
import net.psforever.packet.game.{ObjectDeleteMessage, PlanetSideGUID}
class MultiPacketCollectorTest extends Specification {
val packet1 = ObjectDeleteMessage(PlanetSideGUID(1103), 2)
"MultiPacketBundle" should {
import scodec.bits._
val packet2 = ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)
"construct" in {
MultiPacketBundle(List(packet1))
ok
}
"fail to construct if not initialized with PlanetSidePackets" in {
MultiPacketBundle(Nil) must throwA[IllegalArgumentException]
}
"concatenate bundles into a new bundle" in {
val obj1 = MultiPacketBundle(List(packet1))
val obj2 = MultiPacketBundle(List(packet2))
val obj3 = obj1 + obj2
obj3 match {
case MultiPacketBundle(list) =>
list.size mustEqual 2
list.head mustEqual packet1
list(1) mustEqual packet2
case _ =>
ko
}
}
"accept PlanetSideGamePackets and PlanetSideControlPackets" in {
MultiPacketBundle(List(packet2, packet1)) match {
case MultiPacketBundle(list) =>
list.size mustEqual 2
list.head mustEqual packet2
list(1) mustEqual packet1
case _ =>
ko
}
}
"ignore other types of PlanetSideContainerPackets" in {
val param = List(packet2, ClientFinished(hex"", hex""), packet1, ServerFinished(hex""))
MultiPacketBundle(param) match { //warning message will display in log
case MultiPacketBundle(list) =>
list.size mustEqual 2
list.head mustEqual param.head
list(1) mustEqual param(2)
case _ =>
ko
}
}
}
"MultiPacketCollector" should {
val packet2 = ObjectDeleteMessage(PlanetSideGUID(1105), 2)
val packet3 = ObjectDeleteMessage(PlanetSideGUID(1107), 2)
"construct" in {
new MultiPacketCollector()
ok
}
"construct with initial packets" in {
MultiPacketCollector(List(packet1, packet2))
ok
}
"can retrieve a bundle packets" in {
val obj = MultiPacketCollector(List(packet1, packet2))
obj.Bundle match {
case MultiPacketBundle(list) =>
list.size mustEqual 2
list.head mustEqual packet1
list(1) mustEqual packet2
case _ =>
ko
}
}
"can not retrieve a bundle of non-existent packets" in {
val obj = new MultiPacketCollector()
obj.Bundle must throwA[RuntimeException]
}
"can safely retrieve a bundle of potential packets" in {
val obj1 = new MultiPacketCollector()
obj1.BundleOption match {
case Some(_) =>
ko
case _ => ;
}
val obj2 = MultiPacketCollector(List(packet1, packet2))
obj2.BundleOption match {
case None =>
ko
case Some(MultiPacketBundle(list)) =>
list.size mustEqual 2
list.head mustEqual packet1
list(1) mustEqual packet2
}
}
"clear packets after being asked to bundle" in {
val list = List(packet1, packet2)
val obj = MultiPacketCollector(list)
obj.Bundle mustEqual MultiPacketBundle(list)
obj.Bundle must throwA[RuntimeException]
}
"add a packet" in {
val obj = new MultiPacketCollector()
obj.Add(packet1)
obj.Bundle match {
case MultiPacketBundle(list) =>
list.size mustEqual 1
list.head mustEqual packet1
case _ =>
ko
}
}
"add packets" in {
val obj = new MultiPacketCollector()
obj.Add(List(packet1, packet2))
obj.Bundle match {
case MultiPacketBundle(list) =>
list.size mustEqual 2
list.head mustEqual packet1
list(1) mustEqual packet2
case _ =>
ko
}
}
"concatenate bundles (1)" in {
val obj1 = new MultiPacketCollector()
obj1.Add(List(packet1, packet2))
val bundle1 = obj1.Bundle
val obj2 = MultiPacketCollector(bundle1)
obj2.Add(packet3)
obj2.Bundle match {
case MultiPacketBundle(list) =>
list.size mustEqual 3
list.head mustEqual packet1
list(1) mustEqual packet2
list(2) mustEqual packet3
case _ =>
ko
}
}
"concatenate bundles (2)" in {
val obj1 = new MultiPacketCollector()
obj1.Add(List(packet1, packet2))
val bundle1 = obj1.Bundle
val obj2 = new MultiPacketCollector()
obj2.Add(packet3)
obj2.Add(bundle1)
obj2.Bundle match {
case MultiPacketBundle(list) =>
list.size mustEqual 3
list.head mustEqual packet3
list(1) mustEqual packet1
list(2) mustEqual packet2
case _ =>
ko
}
}
}
}

View file

@ -3,24 +3,80 @@ package control
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.control._
import net.psforever.packet.control.RelatedA
import scodec.bits._
class RelatedATest extends Specification {
val string0 = hex"00 11 01 04"
val string1 = hex"00 12 01 04"
val string2 = hex"00 13 01 04"
val string3 = hex"00 14 01 04"
"decode (0)" in {
PacketCoding.DecodePacket(string0).require match {
case RelatedA0(slot) =>
slot mustEqual 260
case RelatedA(slot, subslot) =>
slot mustEqual 0
subslot mustEqual 260
case _ =>
ko
}
}
"decode (1)" in {
PacketCoding.DecodePacket(string1).require match {
case RelatedA(slot, subslot) =>
slot mustEqual 1
subslot mustEqual 260
case _ =>
ko
}
}
"decode (2)" in {
PacketCoding.DecodePacket(string2).require match {
case RelatedA(slot, subslot) =>
slot mustEqual 2
subslot mustEqual 260
case _ =>
ko
}
}
"decode (3)" in {
PacketCoding.DecodePacket(string3).require match {
case RelatedA(slot, subslot) =>
slot mustEqual 3
subslot mustEqual 260
case _ =>
ko
}
}
"encode (0)" in {
val pkt = RelatedA0(260)
val pkt = RelatedA(0, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string0
}
"encode (1)" in {
val pkt = RelatedA(1, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string1
}
"encode (2)" in {
val pkt = RelatedA(2, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string2
}
"encode (3)" in {
val pkt = RelatedA(3, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string3
}
"encode (n)" in {
RelatedA(4, 260) must throwA[IllegalArgumentException]
}
}

View file

@ -3,24 +3,80 @@ package control
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.control._
import net.psforever.packet.control.RelatedB
import scodec.bits._
class RelatedBTest extends Specification {
val string0 = hex"00 15 01 04"
val string1 = hex"00 16 01 04"
val string2 = hex"00 17 01 04"
val string3 = hex"00 18 01 04"
"decode (0)" in {
PacketCoding.DecodePacket(string0).require match {
case RelatedB0(slot) =>
slot mustEqual 260
case RelatedB(slot, subslot) =>
slot mustEqual 0
subslot mustEqual 260
case _ =>
ko
}
}
"decode (1)" in {
PacketCoding.DecodePacket(string1).require match {
case RelatedB(slot, subslot) =>
slot mustEqual 1
subslot mustEqual 260
case _ =>
ko
}
}
"decode (2)" in {
PacketCoding.DecodePacket(string2).require match {
case RelatedB(slot, subslot) =>
slot mustEqual 2
subslot mustEqual 260
case _ =>
ko
}
}
"decode (3)" in {
PacketCoding.DecodePacket(string3).require match {
case RelatedB(slot, subslot) =>
slot mustEqual 3
subslot mustEqual 260
case _ =>
ko
}
}
"encode (0)" in {
val pkt = RelatedB0(260)
val pkt = RelatedB(0, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string0
}
"encode (1)" in {
val pkt = RelatedB(1, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string1
}
"encode (2)" in {
val pkt = RelatedB(2, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string2
}
"encode (3)" in {
val pkt = RelatedB(3, 260)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string3
}
"encode (n)" in {
RelatedB(4, 260) must throwA[IllegalArgumentException]
}
}

View file

@ -1,29 +0,0 @@
// Copyright (c) 2017 PSForever
package control
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.control._
import scodec.bits._
class SlottedMetaAckTest extends Specification {
val string = hex"00150da4"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case SlottedMetaAck(_, _) =>
ko
case RelatedB0(subslot) => //important!
subslot mustEqual 3492
case _ =>
ko
}
}
"encode" in {
val pkt = SlottedMetaAck(0, 3492)
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
msg mustEqual string
}
}