mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-03-04 13:00:25 +00:00
Merge pull request #158 from Fate-JH/packet-split
Packet Splitting, Once Again
This commit is contained in:
commit
808a40d080
23 changed files with 833 additions and 370 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
common/src/test/scala/control/ControlSyncRespTest.scala
Normal file
32
common/src/test/scala/control/ControlSyncRespTest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
32
common/src/test/scala/control/ControlSyncTest.scala
Normal file
32
common/src/test/scala/control/ControlSyncTest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
30
common/src/test/scala/control/HandleGamePacketTest.scala
Normal file
30
common/src/test/scala/control/HandleGamePacketTest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
43
common/src/test/scala/control/MultiPacketExTest.scala
Normal file
43
common/src/test/scala/control/MultiPacketExTest.scala
Normal 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} }
|
||||
}
|
||||
}
|
||||
}
|
||||
26
common/src/test/scala/control/RelatedATest.scala
Normal file
26
common/src/test/scala/control/RelatedATest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
26
common/src/test/scala/control/RelatedBTest.scala
Normal file
26
common/src/test/scala/control/RelatedBTest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
76
common/src/test/scala/control/SlottedMetaPacketTest.scala
Normal file
76
common/src/test/scala/control/SlottedMetaPacketTest.scala
Normal 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]
|
||||
}
|
||||
}
|
||||
26
common/src/test/scala/control/TeardownConnectionTest.scala
Normal file
26
common/src/test/scala/control/TeardownConnectionTest.scala
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
213
pslogin/src/main/scala/PacketCodingActor.scala
Normal file
213
pslogin/src/main/scala/PacketCodingActor.scala
Normal 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
|
||||
}
|
||||
|
|
@ -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])
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue