mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
[packet] VNLWorldStatusMessage
Added VNL packet type from IDA. Moved definition in to its own file. Refactored PacketCoding MarshalPacket. The whole structure needs a rework. Now able to get a PlanetSide client to the server screen with a server of choice.
This commit is contained in:
parent
1643ecc1dd
commit
b8ff34c0f9
|
|
@ -59,10 +59,5 @@ object ControlPacketOpcode extends Enumeration {
|
||||||
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}"))
|
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val storageType = uint8L
|
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||||
|
|
||||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
|
||||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
|
||||||
|
|
||||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ object GamePacketOpcode extends Enumeration {
|
||||||
type Type = Value
|
type Type = Value
|
||||||
val
|
val
|
||||||
|
|
||||||
// Opcodes should have a marker every 10
|
// Opcodes should have a marker every 10 (decimal)
|
||||||
// OPCODE 0
|
// OPCODE 0
|
||||||
Unknown0,
|
Unknown0,
|
||||||
LoginMessage,
|
LoginMessage,
|
||||||
LoginRespMessage,
|
LoginRespMessage,
|
||||||
Unknown3,
|
Unknown3,
|
||||||
ConnectToWorldMessage,
|
ConnectToWorldMessage,
|
||||||
Unknown5,
|
VNLWorldStatusMessage,
|
||||||
UnknownMessage6,
|
UnknownMessage6,
|
||||||
UnknownMessage7,
|
UnknownMessage7,
|
||||||
PlayerStateMessage,
|
PlayerStateMessage,
|
||||||
|
|
@ -33,13 +33,9 @@ object GamePacketOpcode extends Enumeration {
|
||||||
def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match {
|
def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match {
|
||||||
case LoginMessage => psforever.net.LoginMessage.decode
|
case LoginMessage => psforever.net.LoginMessage.decode
|
||||||
case LoginRespMessage => psforever.net.LoginRespMessage.decode
|
case LoginRespMessage => psforever.net.LoginRespMessage.decode
|
||||||
|
case VNLWorldStatusMessage => psforever.net.VNLWorldStatusMessage.decode
|
||||||
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}"))
|
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val storageType = uint8L
|
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||||
|
|
||||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
|
||||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
|
||||||
|
|
||||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package psforever.net
|
package psforever.net
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
import scodec.{DecodeResult, Err, Codec, Attempt}
|
import scodec.{DecodeResult, Err, Codec, Attempt}
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
|
|
@ -14,22 +16,22 @@ sealed trait PlanetSidePacket extends Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used by companion objects to create encoders and decoders
|
// Used by companion objects to create encoders and decoders
|
||||||
sealed trait Marshallable[T] {
|
trait Marshallable[T] {
|
||||||
implicit val codec : Codec[T]
|
implicit val codec : Codec[T]
|
||||||
def encode(a : T) : Attempt[BitVector] = codec.encode(a)
|
def encode(a : T) : Attempt[BitVector] = codec.encode(a)
|
||||||
// assert that when decoding a marshallable type, that no bits are left over
|
// assert that when decoding a marshallable type, that no bits are left over
|
||||||
def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.complete.decode(a)
|
def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.decode(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed trait PlanetSideGamePacket extends PlanetSidePacket {
|
trait PlanetSideGamePacket extends PlanetSidePacket {
|
||||||
def opcode : GamePacketOpcode.Type
|
def opcode : GamePacketOpcode.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed trait PlanetSideControlPacket extends PlanetSidePacket {
|
trait PlanetSideControlPacket extends PlanetSidePacket {
|
||||||
def opcode : ControlPacketOpcode.Type
|
def opcode : ControlPacketOpcode.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed trait PlanetSideCryptoPacket extends PlanetSidePacket {
|
trait PlanetSideCryptoPacket extends PlanetSidePacket {
|
||||||
def opcode : CryptoPacketOpcode.Type
|
def opcode : CryptoPacketOpcode.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,6 +145,17 @@ object LoginMessage extends Marshallable[LoginMessage] {
|
||||||
|
|
||||||
type Struct = String :: Option[String] :: Option[String] :: HNil
|
type Struct = String :: Option[String] :: Option[String] :: HNil
|
||||||
|
|
||||||
|
/* Okay, okay, here's what's happening here:
|
||||||
|
|
||||||
|
PlanetSide's *wonderful* packet design reuses packets for different encodings.
|
||||||
|
What we have here is that depending on a boolean in the LoginPacket, we will either
|
||||||
|
be decoding a username & password OR a token & username. Yeah...so this doesn't
|
||||||
|
really fit in to a fixed packet decoding scheme.
|
||||||
|
|
||||||
|
The below code abstracts away from this by using pattern matching.
|
||||||
|
The scodec specific part is the either(...) Codec, which decodes one bit and chooses
|
||||||
|
Left or Right depending on it.
|
||||||
|
*/
|
||||||
implicit val credentialChoice : Codec[Struct] = {
|
implicit val credentialChoice : Codec[Struct] = {
|
||||||
type InStruct = Either[String :: String :: HNil, String :: String :: HNil]
|
type InStruct = Either[String :: String :: HNil, String :: String :: HNil]
|
||||||
|
|
||||||
|
|
@ -307,12 +320,7 @@ object PacketType extends Enumeration(1) {
|
||||||
type Type = Value
|
type Type = Value
|
||||||
val ResetSequence, Unknown2, Crypto, Normal = Value
|
val ResetSequence, Unknown2, Crypto, Normal = Value
|
||||||
|
|
||||||
val storageType = uint4L
|
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint4L)
|
||||||
|
|
||||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
|
||||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
|
||||||
|
|
||||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
||||||
|
|
@ -327,10 +335,16 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
|
||||||
// TODO: figure out why I can't insert codecs without using a new case class
|
/*class MarshallableEnum[+T] extends Enumeration {
|
||||||
// Notes: https://mpilquist.github.io/blog/2013/06/09/scodec-part-3/
|
type StorageType = Codec[Int]
|
||||||
// https://stackoverflow.com/questions/29585649/using-nested-case-classes-with-scodec
|
|
||||||
// https://gist.github.com/travisbrown/3945529
|
implicit val storageType : StorageType = uint8
|
||||||
|
|
||||||
|
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
||||||
|
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||||
|
|
||||||
|
implicit val codec: Codec[T] = PacketHelpers.createEnumerationCodec(this, storageType)
|
||||||
|
}*/
|
||||||
|
|
||||||
object PacketHelpers {
|
object PacketHelpers {
|
||||||
def emptyCodec[T](instance : T) = {
|
def emptyCodec[T](instance : T) = {
|
||||||
|
|
@ -339,13 +353,14 @@ object PacketHelpers {
|
||||||
Codec[HNil].xmap[T](from, to)
|
Codec[HNil].xmap[T](from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
|
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
|
||||||
type Struct = Int :: HNil
|
type Struct = Int :: HNil
|
||||||
val struct: Codec[Struct] = storageCodec.hlist
|
val struct: Codec[Struct] = storageCodec.hlist
|
||||||
|
|
||||||
// Assure that the enum will always be able to fit in a N-bit int
|
// Assure that the enum will always be able to fit in a N-bit int
|
||||||
assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2),
|
assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2),
|
||||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
enum.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||||
|
|
||||||
def to(pkt: E#Value): Struct = {
|
def to(pkt: E#Value): Struct = {
|
||||||
pkt.id :: HNil
|
pkt.id :: HNil
|
||||||
|
|
@ -372,15 +387,47 @@ object PacketHelpers {
|
||||||
private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)).
|
private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)).
|
||||||
xmap[Int](
|
xmap[Int](
|
||||||
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
|
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
|
||||||
(a : Int) => if(a > 0x7f) Left(a) else Right(a)
|
(a : Int) =>
|
||||||
|
// if the specified goes above 0x7f (127) then we need two bytes to represent it
|
||||||
|
if(a > 0x7f) Left(a) else Right(a)
|
||||||
)
|
)
|
||||||
|
|
||||||
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = either(bool, uint(15), uint(7)).
|
/*private def encodedStringSizeWithLimit(limit : Int) : Codec[Int] = {
|
||||||
xmap[Int](
|
either(bool, uint(15), uint(7)).
|
||||||
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
|
exmap[Int](
|
||||||
(a : Int) => if(a > 0x7f) Left(a) else Right(a)
|
(a : Either[Int, Int]) => {
|
||||||
) <~ ignore(pad)
|
val result = a.fold[Int](a => a, a => a)
|
||||||
|
|
||||||
|
if(result > limit)
|
||||||
|
Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))
|
||||||
|
else
|
||||||
|
Attempt.successful(result)
|
||||||
|
},
|
||||||
|
(a : Int) => {
|
||||||
|
if(a > limit)
|
||||||
|
return Attempt.failure(Err("adsf"))
|
||||||
|
//return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")))
|
||||||
|
|
||||||
|
if(a > 0x7f)
|
||||||
|
return Attempt.successful(Left(a))
|
||||||
|
else
|
||||||
|
Right(a)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = encodedStringSize <~ ignore(pad)
|
||||||
|
|
||||||
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
|
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
|
||||||
|
//def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii)
|
||||||
def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii)
|
def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii)
|
||||||
|
|
||||||
|
/// Variable for the charset that planetside uses for unicode
|
||||||
|
val utf16 = string(Charset.forName("UTF-16LE"))
|
||||||
|
|
||||||
|
/// An encoded *wide* string is twice the length of the given encoded size and half of the length of the
|
||||||
|
/// input string. We use xmap to transform the encodedString codec as this change is just a division and multiply
|
||||||
|
def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
|
||||||
|
insize => insize*2,
|
||||||
|
outSize => outSize/2), utf16)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,41 @@ import scodec.bits._
|
||||||
import scodec.{DecodeResult, Err, Attempt, Codec}
|
import scodec.{DecodeResult, Err, Attempt, Codec}
|
||||||
import scodec.codecs.{uint16L, uint8L, uint4L, bytes}
|
import scodec.codecs.{uint16L, uint8L, uint4L, bytes}
|
||||||
|
|
||||||
// Packet containers
|
/// Packet container base trait
|
||||||
sealed trait PlanetSidePacketContainer
|
sealed trait PlanetSidePacketContainer
|
||||||
|
|
||||||
// A sequence, encrypted opcode, encrypted payload, and MD5MAC plus padding
|
/// A sequence, encrypted opcode, encrypted payload, and implicit MD5MAC plus padding
|
||||||
final case class EncryptedPacket(sequenceNumber : Int,
|
final case class EncryptedPacket(sequenceNumber : Int,
|
||||||
payload : ByteVector) extends PlanetSidePacketContainer
|
payload : ByteVector) extends PlanetSidePacketContainer
|
||||||
|
|
||||||
// A sequence, and payload. Crypto packets have no discernible opcodes
|
/// A sequence, and payload. Crypto packets have no discernible opcodes an rely off of implicit
|
||||||
|
/// state to decode properly
|
||||||
final case class CryptoPacket(sequenceNumber : Int,
|
final case class CryptoPacket(sequenceNumber : Int,
|
||||||
packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer
|
packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer
|
||||||
|
|
||||||
|
/// A sequenced game packet with an opcode and payload
|
||||||
final case class GamePacket(opcode : GamePacketOpcode.Value,
|
final case class GamePacket(opcode : GamePacketOpcode.Value,
|
||||||
sequenceNumber : Int,
|
sequenceNumber : Int,
|
||||||
packet : PlanetSideGamePacket) extends PlanetSidePacketContainer
|
packet : PlanetSideGamePacket) extends PlanetSidePacketContainer
|
||||||
|
|
||||||
// Just an opcode + payload (does not expect a response)
|
/// Just an opcode + payload
|
||||||
final case class ControlPacket(opcode : ControlPacketOpcode.Value,
|
final case class ControlPacket(opcode : ControlPacketOpcode.Value,
|
||||||
packet : PlanetSideControlPacket) extends PlanetSidePacketContainer
|
packet : PlanetSideControlPacket) extends PlanetSidePacketContainer
|
||||||
|
|
||||||
object PacketCoding {
|
object PacketCoding {
|
||||||
|
/// A lower bound on the packet size
|
||||||
final val PLANETSIDE_MIN_PACKET_SIZE = 2
|
final val PLANETSIDE_MIN_PACKET_SIZE = 2
|
||||||
|
|
||||||
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
|
/**
|
||||||
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
|
* Given a full and complete planetside packet as it would be sent on the wire, attempt to
|
||||||
}
|
* decode it given an optional header and required payload. This function does all of the
|
||||||
|
* hard work of making decisions along the way in order to decode a planetside packet to
|
||||||
|
* completion.
|
||||||
|
* @param msg the raw packet
|
||||||
|
* @param cryptoState the current state of the connection's crypto. This is only used when decoding
|
||||||
|
* crypto packets as they do not have opcodes
|
||||||
|
* @return PlanetSidePacketContainer
|
||||||
|
*/
|
||||||
def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
|
def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
|
||||||
// check for a minimum length
|
// check for a minimum length
|
||||||
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||||
|
|
@ -47,6 +56,22 @@ object PacketCoding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to decode a packet without specifying a crypto packet state.
|
||||||
|
* Mostly used when there is no crypto state available, such as tests.
|
||||||
|
* @param msg packet data bytes
|
||||||
|
* @return PlanetSidePacketContainer
|
||||||
|
*/
|
||||||
|
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
|
||||||
|
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to UnmarshalPacket, but does not process any packet header and does not support
|
||||||
|
* decoding of crypto packets. Mostly used in tests.
|
||||||
|
* @param msg raw, unencrypted packet
|
||||||
|
* @return PlanetSidePacket
|
||||||
|
*/
|
||||||
def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = {
|
def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = {
|
||||||
// check for a minimum length
|
// check for a minimum length
|
||||||
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||||
|
|
@ -68,70 +93,110 @@ object PacketCoding {
|
||||||
var opcodeEncoded : BitVector = BitVector.empty
|
var opcodeEncoded : BitVector = BitVector.empty
|
||||||
var payloadEncoded : BitVector = BitVector.empty
|
var payloadEncoded : BitVector = BitVector.empty
|
||||||
|
|
||||||
|
var controlPacket = false
|
||||||
|
var sequenceNum = 0
|
||||||
|
|
||||||
|
// packet flags
|
||||||
|
var hasFlags = true
|
||||||
|
var secured = false
|
||||||
|
var packetType = PacketType.Crypto
|
||||||
|
|
||||||
packet match {
|
packet match {
|
||||||
case GamePacket(opcode, seq, payload) =>
|
case GamePacket(opcode, seq, payload) =>
|
||||||
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false)
|
secured = false
|
||||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
packetType = PacketType.Normal
|
||||||
|
sequenceNum = seq
|
||||||
|
|
||||||
GamePacketOpcode.codec.encode(opcode) match {
|
EncodePacket(payload) match {
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext))
|
case f @ Failure(e) => return f
|
||||||
case Successful(p) => opcodeEncoded = p
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16L.encode(seq) match {
|
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $opcode: " + e.messageWithContext))
|
|
||||||
case Successful(p) => seqEncoded = p
|
|
||||||
}
|
|
||||||
|
|
||||||
encodePacket(payload) match {
|
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
|
||||||
case Successful(p) => payloadEncoded = p
|
case Successful(p) => payloadEncoded = p
|
||||||
}
|
}
|
||||||
case ControlPacket(opcode, payload) =>
|
case ControlPacket(opcode, payload) =>
|
||||||
flagsEncoded = hex"00".bits
|
controlPacket = true
|
||||||
|
|
||||||
ControlPacketOpcode.codec.encode(opcode) match {
|
EncodePacket(payload) match {
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext))
|
case f @ Failure(e) => return f
|
||||||
case Successful(p) => opcodeEncoded = p
|
|
||||||
}
|
|
||||||
|
|
||||||
encodePacket(payload) match {
|
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
|
||||||
case Successful(p) => payloadEncoded = p
|
case Successful(p) => payloadEncoded = p
|
||||||
}
|
}
|
||||||
case CryptoPacket(seq, payload) =>
|
case CryptoPacket(seq, payload) =>
|
||||||
val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false)
|
secured = false
|
||||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
packetType = PacketType.Crypto
|
||||||
|
sequenceNum = seq
|
||||||
|
|
||||||
uint16L.encode(seq) match {
|
EncodePacket(payload) match {
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext))
|
case f @ Failure(e) => return f
|
||||||
case Successful(p) => seqEncoded = p
|
|
||||||
}
|
|
||||||
|
|
||||||
encodePacket(payload) match {
|
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $payload: " + e.messageWithContext))
|
|
||||||
case Successful(p) => payloadEncoded = p
|
case Successful(p) => payloadEncoded = p
|
||||||
}
|
}
|
||||||
case EncryptedPacket(seq, payload) =>
|
case EncryptedPacket(seq, payload) =>
|
||||||
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true)
|
secured = true
|
||||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
packetType = PacketType.Normal
|
||||||
|
sequenceNum = seq
|
||||||
|
|
||||||
// encrypted packets need to be aligned to 4 bytes before encryption/decryption
|
// 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
|
// first byte are flags, second and third the sequence, and fourth is the pad
|
||||||
paddingEncoded = hex"00".bits
|
paddingEncoded = hex"00".bits
|
||||||
|
|
||||||
uint16L.encode(seq) match {
|
|
||||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext))
|
|
||||||
case Successful(p) => seqEncoded = p
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadEncoded = payload.bits
|
payloadEncoded = payload.bits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val flags = PlanetSidePacketFlags(packetType, secured = secured)
|
||||||
|
|
||||||
|
// crypto packets DONT have flags
|
||||||
|
if(!controlPacket) {
|
||||||
|
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
||||||
|
|
||||||
|
uint16L.encode(sequenceNum) match {
|
||||||
|
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $packet: " + e.messageWithContext))
|
||||||
|
case Successful(p) => seqEncoded = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded
|
val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded
|
||||||
Attempt.successful(finalPacket)
|
Attempt.successful(finalPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def EncodePacket(packet : PlanetSideControlPacket) : Attempt[BitVector] = {
|
||||||
|
val opcode = packet.opcode
|
||||||
|
var opcodeEncoded = BitVector.empty
|
||||||
|
var payloadEncoded = BitVector.empty
|
||||||
|
|
||||||
|
ControlPacketOpcode.codec.encode(opcode) match {
|
||||||
|
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in control packet $opcode: " + e.messageWithContext))
|
||||||
|
case Successful(p) => opcodeEncoded = p
|
||||||
|
}
|
||||||
|
|
||||||
|
encodePacket(packet) match {
|
||||||
|
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext))
|
||||||
|
case Successful(p) => payloadEncoded = p
|
||||||
|
}
|
||||||
|
|
||||||
|
Attempt.Successful(hex"00".bits ++ opcodeEncoded ++ payloadEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
def EncodePacket(packet : PlanetSideCryptoPacket) : Attempt[BitVector] = {
|
||||||
|
encodePacket(packet) match {
|
||||||
|
case Failure(e) => Attempt.failure(Err(s"Failed to marshal crypto packet $packet: " + e.messageWithContext))
|
||||||
|
case s @ Successful(p) => s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def EncodePacket(packet : PlanetSideGamePacket) : Attempt[BitVector] = {
|
||||||
|
val opcode = packet.opcode
|
||||||
|
var opcodeEncoded = BitVector.empty
|
||||||
|
var payloadEncoded = BitVector.empty
|
||||||
|
|
||||||
|
GamePacketOpcode.codec.encode(opcode) match {
|
||||||
|
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in game packet $opcode: " + e.messageWithContext))
|
||||||
|
case Successful(p) => opcodeEncoded = p
|
||||||
|
}
|
||||||
|
|
||||||
|
encodePacket(packet) match {
|
||||||
|
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext))
|
||||||
|
case Successful(p) => payloadEncoded = p
|
||||||
|
}
|
||||||
|
|
||||||
|
Attempt.Successful(opcodeEncoded ++ payloadEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet)
|
def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet)
|
||||||
def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet)
|
def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet)
|
||||||
def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet)
|
def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet)
|
||||||
|
|
@ -199,6 +264,7 @@ object PacketCoding {
|
||||||
val packet = DecodeControlPacket(msg)
|
val packet = DecodeControlPacket(msg)
|
||||||
|
|
||||||
packet match {
|
packet match {
|
||||||
|
// just return the failure
|
||||||
case f @ Failure(e) => f
|
case f @ Failure(e) => f
|
||||||
case Successful(p) =>
|
case Successful(p) =>
|
||||||
Attempt.successful(CreateControlPacket(p))
|
Attempt.successful(CreateControlPacket(p))
|
||||||
|
|
@ -271,7 +337,7 @@ object PacketCoding {
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
|
|
||||||
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = {
|
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = {
|
||||||
// TODO: this is bad. rework
|
// TODO XXX: this is bad. rework
|
||||||
var sequenceNumber = 0
|
var sequenceNumber = 0
|
||||||
|
|
||||||
val rawPacket : BitVector = packet match {
|
val rawPacket : BitVector = packet match {
|
||||||
|
|
@ -303,10 +369,14 @@ object PacketCoding {
|
||||||
case default => throw new IllegalArgumentException("Unsupported packet container type")
|
case default => throw new IllegalArgumentException("Unsupported packet container type")
|
||||||
}
|
}
|
||||||
|
|
||||||
val packetMac = crypto.macForEncrypt(rawPacket.toByteVector)
|
encryptPacket(crypto, sequenceNumber, rawPacket.toByteVector)
|
||||||
|
}
|
||||||
|
|
||||||
|
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, sequenceNumber : Int, rawPacket : ByteVector) : Attempt[EncryptedPacket] = {
|
||||||
|
val packetMac = crypto.macForEncrypt(rawPacket)
|
||||||
|
|
||||||
// opcode, payload, and MAC
|
// opcode, payload, and MAC
|
||||||
val packetNoPadding = rawPacket.toByteVector ++ packetMac
|
val packetNoPadding = rawPacket ++ packetMac
|
||||||
|
|
||||||
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE
|
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
|
package psforever.net
|
||||||
|
import scodec._
|
||||||
|
import scodec.bits._
|
||||||
|
import scodec.codecs._
|
||||||
|
import shapeless._
|
||||||
|
|
||||||
|
object WorldStatus extends Enumeration {
|
||||||
|
type Type = Value
|
||||||
|
val Up, Down, Locked, Full = Value
|
||||||
|
}
|
||||||
|
|
||||||
|
object ServerType extends Enumeration {
|
||||||
|
type Type = Value
|
||||||
|
val Development, Beta, Released = Value
|
||||||
|
|
||||||
|
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||||
|
}
|
||||||
|
|
||||||
|
object EmpireNeed extends Enumeration {
|
||||||
|
type Type = Value
|
||||||
|
val TR, NC, VS = Value
|
||||||
|
|
||||||
|
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class WorldInformation(name : String, status : WorldStatus.Value,
|
||||||
|
serverType : ServerType.Value, empireNeed : EmpireNeed.Value)
|
||||||
|
|
||||||
|
final case class VNLWorldStatusMessage(welcomeMessage : String, worlds : Vector[WorldInformation])
|
||||||
|
extends PlanetSideGamePacket {
|
||||||
|
type Packet = VNLWorldStatusMessage
|
||||||
|
def opcode = GamePacketOpcode.VNLWorldStatusMessage
|
||||||
|
def encode = VNLWorldStatusMessage.encode(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
object VNLWorldStatusMessage extends Marshallable[VNLWorldStatusMessage] {
|
||||||
|
type InStruct = WorldStatus.Value :: ServerType.Value :: HNil
|
||||||
|
type OutStruct = Int :: ServerType.Value :: Int :: HNil
|
||||||
|
|
||||||
|
implicit val statusCodec : Codec[InStruct] = {
|
||||||
|
def from(a : InStruct) : OutStruct = a match {
|
||||||
|
case status :: svrType :: HNil =>
|
||||||
|
status match {
|
||||||
|
case WorldStatus.Down =>
|
||||||
|
0 :: svrType :: 2 :: HNil
|
||||||
|
case WorldStatus.Locked =>
|
||||||
|
0 :: svrType :: 1 :: HNil
|
||||||
|
case WorldStatus.Up =>
|
||||||
|
1 :: svrType :: 0 :: HNil
|
||||||
|
case WorldStatus.Full =>
|
||||||
|
5 :: svrType :: 0 :: HNil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def to(a : OutStruct) : InStruct = a match {
|
||||||
|
case status2 :: svrType :: status1 :: HNil =>
|
||||||
|
if(status1 == 0) {
|
||||||
|
if(status2 >= 5) {
|
||||||
|
WorldStatus.Full :: svrType :: HNil
|
||||||
|
} else {
|
||||||
|
WorldStatus.Up :: svrType :: HNil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(status1 != 1)
|
||||||
|
WorldStatus.Down :: svrType :: HNil
|
||||||
|
else
|
||||||
|
WorldStatus.Locked :: svrType :: HNil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(("status2" | uint16L) ::
|
||||||
|
("server_type" | ServerType.codec) ::
|
||||||
|
("status1" | uint8L)).xmap(to, from)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit val codec : Codec[VNLWorldStatusMessage] = (
|
||||||
|
("welcome_message" | PacketHelpers.encodedWideString) ::
|
||||||
|
("worlds" | vectorOfN(uint8L, (
|
||||||
|
// XXX: this needs to be limited to 0x20 bytes
|
||||||
|
// XXX: this needs to be byte aligned, but not sure how to do this
|
||||||
|
("world_name" | PacketHelpers.encodedString) :: (
|
||||||
|
("status_and_type" | statusCodec) :+
|
||||||
|
("unknown" | constant(hex"01459e25403775")) :+
|
||||||
|
("empire_need" | EmpireNeed.codec)
|
||||||
|
)
|
||||||
|
).as[WorldInformation]
|
||||||
|
))).as[VNLWorldStatusMessage]
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import psforever.crypto.CryptoInterface
|
import psforever.crypto.CryptoInterface
|
||||||
import psforever.crypto.CryptoInterface.{CryptoState, CryptoDHState}
|
import psforever.crypto.CryptoInterface.CryptoDHState
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class CryptoInterfaceTest extends Specification {
|
class CryptoInterfaceTest extends Specification {
|
||||||
|
|
@ -3,7 +3,7 @@ import psforever.net._
|
||||||
import scodec.Codec
|
import scodec.Codec
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class CryptoPackets extends Specification {
|
class CryptoPacketTest extends Specification {
|
||||||
|
|
||||||
"PlanetSide crypto packet" in {
|
"PlanetSide crypto packet" in {
|
||||||
val cNonce = 656287232
|
val cNonce = 656287232
|
||||||
54
common/src/test/scala/GamePacketTest.scala
Normal file
54
common/src/test/scala/GamePacketTest.scala
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
|
import org.specs2.mutable._
|
||||||
|
import psforever.net._
|
||||||
|
import scodec.bits._
|
||||||
|
|
||||||
|
class GamePacketTest extends Specification {
|
||||||
|
|
||||||
|
"PlanetSide game packet" in {
|
||||||
|
val cNonce = 656287232
|
||||||
|
|
||||||
|
"VNLWorldStatusMessage" should {
|
||||||
|
val string = hex"0597570065006c0063006f006d006500200074006f00200050006c0061006e00650074005300690064006500210020000186" ++
|
||||||
|
hex"67656d696e69" ++ hex"0100 01 00 01459e2540 3775" ++ bin"01".toByteVector
|
||||||
|
|
||||||
|
"decode" in {
|
||||||
|
PacketCoding.DecodePacket(string).require match {
|
||||||
|
case VNLWorldStatusMessage(message, worlds) =>
|
||||||
|
worlds.length mustEqual 1
|
||||||
|
message mustEqual "Welcome to PlanetSide! "
|
||||||
|
worlds{0}.name mustEqual "gemini"
|
||||||
|
worlds{0}.empireNeed mustEqual EmpireNeed.NC
|
||||||
|
worlds{0}.status mustEqual WorldStatus.Up
|
||||||
|
case default =>
|
||||||
|
true mustEqual false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode" in {
|
||||||
|
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
|
||||||
|
Vector(WorldInformation("gemini", WorldStatus.Up, ServerType.Beta, EmpireNeed.NC)))
|
||||||
|
//0100 04 00 01459e2540377540
|
||||||
|
|
||||||
|
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
|
pkt mustEqual string
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode and decode multiple worlds" in {
|
||||||
|
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
|
||||||
|
Vector(
|
||||||
|
WorldInformation("PSForever1", WorldStatus.Up, ServerType.Released, EmpireNeed.NC),
|
||||||
|
WorldInformation("PSForever2", WorldStatus.Down, ServerType.Beta, EmpireNeed.TR)
|
||||||
|
))
|
||||||
|
|
||||||
|
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
|
println(pkt)
|
||||||
|
|
||||||
|
true mustEqual true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import psforever.crypto.CryptoInterface
|
|
||||||
import psforever.net._
|
import psforever.net._
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import scodec.{Err, Attempt, Codec}
|
||||||
import scodec.codecs.{uint16L, uint8L, bytes}
|
import scodec.codecs.{uint16L, uint8L, bytes}
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
/*sealed trait SessionState extends Serializable
|
/**
|
||||||
final case class NewSession() extends SessionState
|
* Actor that stores crypto state for a connection and filters away any packet metadata.
|
||||||
final case class EstablishSecureChannel() extends SessionState
|
* Also decrypts and handles packet retries using the sequence numbers.
|
||||||
final case class SessionDead() extends SessionState*/
|
* @param session Per session state
|
||||||
|
*/
|
||||||
class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging {
|
class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging {
|
||||||
var cryptoDHState = new CryptoInterface.CryptoDHState()
|
var cryptoDHState = new CryptoInterface.CryptoDHState()
|
||||||
var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None
|
var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None
|
||||||
|
|
@ -27,10 +27,11 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
var clientChallenge = ByteVector.empty
|
var clientChallenge = ByteVector.empty
|
||||||
var clientChallengeResult = ByteVector.empty
|
var clientChallengeResult = ByteVector.empty
|
||||||
|
|
||||||
def receive = clientStart
|
def receive = NewClient
|
||||||
|
|
||||||
def clientStart : Receive = {
|
def NewClient : Receive = {
|
||||||
case RawPacket(msg) =>
|
case RawPacket(msg) =>
|
||||||
|
// PacketCoding.DecodePacket
|
||||||
PacketCoding.UnmarshalPacket(msg) match {
|
PacketCoding.UnmarshalPacket(msg) match {
|
||||||
case Failure(e) => log.error("Could not decode packet: " + e)
|
case Failure(e) => log.error("Could not decode packet: " + e)
|
||||||
case Successful(p) =>
|
case Successful(p) =>
|
||||||
|
|
@ -39,7 +40,8 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
p match {
|
p match {
|
||||||
case ControlPacket(_, ClientStart(nonce)) =>
|
case ControlPacket(_, ClientStart(nonce)) =>
|
||||||
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
|
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
|
||||||
context.become(clientXchg)
|
|
||||||
|
context.become(CryptoExchange)
|
||||||
case default =>
|
case default =>
|
||||||
log.error("Unexpected packet type " + p)
|
log.error("Unexpected packet type " + p)
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +49,7 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
case default => log.error(s"Invalid message received ${default}")
|
case default => log.error(s"Invalid message received ${default}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def clientXchg : Receive = {
|
def CryptoExchange : Receive = {
|
||||||
case RawPacket(msg) =>
|
case RawPacket(msg) =>
|
||||||
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
|
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
|
||||||
case Failure(e) => log.error("Could not decode packet: " + e)
|
case Failure(e) => log.error("Could not decode packet: " + e)
|
||||||
|
|
@ -79,14 +81,14 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
// save the sent packet a MAC check
|
// save the sent packet a MAC check
|
||||||
serverMACBuffer ++= sentPacket.drop(3)
|
serverMACBuffer ++= sentPacket.drop(3)
|
||||||
|
|
||||||
context.become(clientFinished)
|
context.become(CryptoSetupFinishing)
|
||||||
case default => log.error("Unexpected packet type " + p)
|
case default => log.error("Unexpected packet type " + p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case default => log.error(s"Invalid message received ${default}")
|
case default => log.error(s"Invalid message received ${default}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def clientFinished : Receive = {
|
def CryptoSetupFinishing : Receive = {
|
||||||
case RawPacket(msg) =>
|
case RawPacket(msg) =>
|
||||||
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
|
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
|
||||||
case Failure(e) => log.error("Could not decode packet: " + e)
|
case Failure(e) => log.error("Could not decode packet: " + e)
|
||||||
|
|
@ -163,14 +165,14 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
|
|
||||||
sendResponse(packet)
|
sendResponse(packet)
|
||||||
|
|
||||||
context.become(established)
|
context.become(Established)
|
||||||
case default => failWithError("Unexpected packet type " + default)
|
case default => failWithError("Unexpected packet type " + default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case default => failWithError(s"Invalid message received ${default}")
|
case default => failWithError(s"Invalid message received ${default}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def established : Receive = {
|
def Established : Receive = {
|
||||||
case RawPacket(msg) =>
|
case RawPacket(msg) =>
|
||||||
PacketCoding.UnmarshalPacket(msg) match {
|
PacketCoding.UnmarshalPacket(msg) match {
|
||||||
case Successful(p) =>
|
case Successful(p) =>
|
||||||
|
|
@ -204,6 +206,15 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
|
||||||
))).require
|
))).require
|
||||||
|
|
||||||
sendResponse(packet)
|
sendResponse(packet)
|
||||||
|
|
||||||
|
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
|
||||||
|
Vector(
|
||||||
|
WorldInformation("gemini", WorldStatus.Up, ServerType.Released, EmpireNeed.NC)
|
||||||
|
))
|
||||||
|
|
||||||
|
sendResponse(PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(4,
|
||||||
|
msg
|
||||||
|
)).require)
|
||||||
case Failure(e) => println("Failed to decode inner packet " + e)
|
case Failure(e) => println("Failed to decode inner packet " + e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue