mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Networking
The game uses a UDP-based protocol. Unlike TCP, UDP does not guarantee that packets arrive, or that they arrive in the correct order. For this reason, the game protocol implements those features using the following: * All packets have a sequence number that is utilized for reordering * Important packets are wrapped in a SlottedMetaPacket with a subslot number * RelatedA packets ae used to request lost packets using the subslot number * RelatedB packets are used to confirm received SlottedMetaPackets All of these go both ways, server <-> client. We used to only partially implement these features: Outgoing packet bundles used SMPs and could be resent, but not all packets were bundled and there was no logic for requesting lost packets from the client and there was no packet reordering, which resulted in dire consequences in the case of packet loss (zoning failures, crashes and many other odd bugs). This patch addresses all of these issues. * Packet bundling: Packets are now automatically bundled and sent as SlottedMetaPackets using a recurring timer. All manual bundling functionality was removed. * Packet reordering: Incoming packets, if received out of order, are stashed and reordered. The maximum wait time for reordering is 20ms. * Packet requesting: Missing SlottedMetaPackets are requested from the client. * PacketCoding refactor: Dropped confusing packet container types. Fixes #5. * Crypto rewrite: PSCrypto is based on a ancient buggy version of cryptopp. Updating to a current version was not possible because it removed the MD5-MAC algorithm. For more details, see Md5Mac.scala. This patch replaces PSCrypto with native Scala code. * Added two new actors: * SocketActor: A simple typed UDP socket actor * MiddlewareActor: The old session pipeline greatly simplified into a typed actor that does most of the things mentioned above. * Begun work on a headless client * Fixed anniversary gun breaking stamina regen * Resolved a few sentry errors
This commit is contained in:
parent
5827204b10
commit
407429ee21
114
.codecov.yml
114
.codecov.yml
|
|
@ -2,60 +2,60 @@
|
|||
comment: off
|
||||
|
||||
ignore:
|
||||
- "common/src/main/scala/net/psforever/objects/ObjectType.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/avatar/Avatars.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/equipment/Ammo.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/equipment/CItem.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/equipment/Kits.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/equipment/SItem.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestiction.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/DestroyedVehicle.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/SeatArmoRestriction.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/projectile/ProjectileCalculations.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceProfile.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/DamageType.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/StandardResistanceProfile.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala"
|
||||
- "common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/crypto"
|
||||
- "common/src/main/scala/net/psforever/packet/game/objectcreate/DrawnSlot.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/game/objectcreate/DriveState.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala"
|
||||
- "common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala"
|
||||
- "common/src/main/scala/net/psforever/types/Angular.scala"
|
||||
- "common/src/main/scala/net/psforever/types/CertificationType.scala"
|
||||
- "common/src/main/scala/net/psforever/types/ChatMessageType.scala"
|
||||
- "common/src/main/scala/net/psforever/types/DriveState.scala"
|
||||
- "common/src/main/scala/net/psforever/types/EmoteType.scala"
|
||||
- "common/src/main/scala/net/psforever/types/ExoSuitType.scala"
|
||||
- "common/src/main/scala/net/psforever/types/GrenadeState.scala"
|
||||
- "common/src/main/scala/net/psforever/types/ImplantType.scala"
|
||||
- "common/src/main/scala/net/psforever/types/MeritCommendation.scala"
|
||||
- "common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala"
|
||||
- "common/src/main/scala/net/psforever/types/TransactionType.scala"
|
||||
- "common/src/main/scala/net.psforever.services/avatar/AvatarAction.scala"
|
||||
- "common/src/main/scala/net.psforever.services/avatar/AvatarResponse.scala"
|
||||
- "common/src/main/scala/net.psforever.services/galaxy/GalaxyAction.scala"
|
||||
- "common/src/main/scala/net.psforever.services/galaxy/GalaxyResponse.scala"
|
||||
- "common/src/main/scala/net.psforever.services/local/LocalAction.scala"
|
||||
- "common/src/main/scala/net.psforever.services/local/LocalResponse.scala"
|
||||
- "common/src/main/scala/net.psforever.services/vehicle/VehicleAction.scala"
|
||||
- "common/src/main/scala/net.psforever.services/vehicle/VehicleResponse.scala"
|
||||
- "src/main/scala/net/psforever/objects/ObjectType.scala"
|
||||
- "src/main/scala/net/psforever/objects/avatar/Avatars.scala"
|
||||
- "src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala"
|
||||
- "src/main/scala/net/psforever/objects/ballistics/Projectiles.scala"
|
||||
- "src/main/scala/net/psforever/objects/equipment/Ammo.scala"
|
||||
- "src/main/scala/net/psforever/objects/equipment/CItem.scala"
|
||||
- "src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala"
|
||||
- "src/main/scala/net/psforever/objects/equipment/Kits.scala"
|
||||
- "src/main/scala/net/psforever/objects/equipment/SItem.scala"
|
||||
- "src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestiction.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/DestroyedVehicle.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/SeatArmoRestriction.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/Turrets.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/projectile/ProjectileCalculations.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/resistance/ResistanceProfile.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/DamageType.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/StandardDamages.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/StandardResistanceProfile.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/StandardResistances.scala"
|
||||
- "src/main/scala/net/psforever/objects/vital/StandardResolutions.scala"
|
||||
- "src/main/scala/net/psforever/packet/crypto"
|
||||
- "src/main/scala/net/psforever/packet/game/objectcreate/DrawnSlot.scala"
|
||||
- "src/main/scala/net/psforever/packet/game/objectcreate/DriveState.scala"
|
||||
- "src/main/scala/net/psforever/packet/game/objectcreate/MountItem.scala"
|
||||
- "src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala"
|
||||
- "src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala"
|
||||
- "src/main/scala/net/psforever/packet/ControlPacketOpcode.scala"
|
||||
- "src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala"
|
||||
- "src/main/scala/net/psforever/packet/GamePacketOpcode.scala"
|
||||
- "src/main/scala/net/psforever/types/Angular.scala"
|
||||
- "src/main/scala/net/psforever/types/CertificationType.scala"
|
||||
- "src/main/scala/net/psforever/types/ChatMessageType.scala"
|
||||
- "src/main/scala/net/psforever/types/DriveState.scala"
|
||||
- "src/main/scala/net/psforever/types/EmoteType.scala"
|
||||
- "src/main/scala/net/psforever/types/ExoSuitType.scala"
|
||||
- "src/main/scala/net/psforever/types/GrenadeState.scala"
|
||||
- "src/main/scala/net/psforever/types/ImplantType.scala"
|
||||
- "src/main/scala/net/psforever/types/MeritCommendation.scala"
|
||||
- "src/main/scala/net/psforever/types/PlanetSideEmpire.scala"
|
||||
- "src/main/scala/net/psforever/types/TransactionType.scala"
|
||||
- "src/main/scala/net/psforever/services/avatar/AvatarAction.scala"
|
||||
- "src/main/scala/net/psforever/services/avatar/AvatarResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/galaxy/GalaxyAction.scala"
|
||||
- "src/main/scala/net/psforever/services/galaxy/GalaxyResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/local/LocalAction.scala"
|
||||
- "src/main/scala/net/psforever/services/local/LocalResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala"
|
||||
- "src/main/scala/net/psforever/services/vehicle/VehicleResponse.scala"
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
|
@ -29,8 +29,6 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
- name: Setup Scala
|
||||
uses: olafurpg/setup-scala@v5
|
||||
- name: Install pscrypto
|
||||
run: curl -L https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip | jar vx
|
||||
- name: Run migrations
|
||||
run: sbt "server/run migrate"
|
||||
- name: Run build
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,6 +12,7 @@ out/
|
|||
.metals
|
||||
project/metals.sbt
|
||||
/docs
|
||||
.vscode
|
||||
|
||||
# User configs
|
||||
config/psforever.conf
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ COPY . /PSF-LoginServer
|
|||
|
||||
WORKDIR /PSF-LoginServer
|
||||
|
||||
RUN wget https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip && \
|
||||
unzip pscrypto-lib-1.1.zip && rm pscrypto-lib-1.1.zip && \
|
||||
sbt server/pack
|
||||
RUN sbt server/pack
|
||||
|
||||
FROM openjdk:8-slim
|
||||
|
||||
|
|
@ -16,4 +14,4 @@ EXPOSE 51000
|
|||
EXPOSE 51001
|
||||
EXPOSE 51002
|
||||
|
||||
CMD ["psf-server"]
|
||||
CMD ["psforever-server"]
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -23,7 +23,6 @@ instructions on downloading the game and using the PSForever launcher to start t
|
|||
|
||||
- sbt (Scala build tool)
|
||||
- Java Development Kit (JDK) 8.0
|
||||
- PSCrypto v1.1 - binary DLL (Windows) or Shared Library (Linux) placed in the root directory of the project. See [Downloading PSCrypto](#downloading-pscrypto) to get it set up.
|
||||
- PostgreSQL
|
||||
|
||||
## Setting up a Build Environment
|
||||
|
|
@ -80,19 +79,6 @@ DB](#setting-up-the-database)). Note: sbt is quite slow at starting up (JVM/JIT
|
|||
open sbt console (just run `sbt` without any arguments) in order to avoid this startup time. With a sbt console you can
|
||||
run tests (and you should) using `sbt test`.
|
||||
|
||||
### Downloading PSCrypto
|
||||
**The server requires binary builds of PSCrypto in order to run.** [Download the latest
|
||||
*release](https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip) and extract the the
|
||||
*approprate dll for your operating system. If you are not comfortable with compiled binaries, you can [build the
|
||||
*libraries yourself](https://github.com/psforever/PSCrypto).
|
||||
|
||||
sbt, IDEA, and Java will automatically find the required libraries when running the server. The build expects to find
|
||||
the library in a subdirectory of the root directory called /pscrypto-lib/. Historically, we have recommended placing it
|
||||
directly into the root directory and that has worked as well. If you still have issues with PSCrypto being detected, try
|
||||
adding `-Djava.library.path=` (no path necessary) to your preferred IDE's build configuration with the library in the
|
||||
root directory. For example, with IDEA: Run -> Edit Configuration -> (select the configuration) -> Uncheck "Use sbt
|
||||
shell" -> VM Parameters
|
||||
|
||||
## Setting up the Database
|
||||
|
||||
The Login and World servers require PostgreSQL for persistence.
|
||||
|
|
@ -194,7 +180,7 @@ some helper scripts. Run the correct file for your platform (.BAT for Windows an
|
|||
|
||||
Using sbt, you can generate documentation all projects using `sbt docs/unidoc`.
|
||||
|
||||
Current documentation is available at [https://jgillich.github.io/PSF-LoginServer/net/psforever/index.html](https://jgillich.github.io/PSF-LoginServer/net/psforever/index.html)
|
||||
Current documentation is available at [https://psforever.github.io/PSF-LoginServer/net/psforever/index.html](https://psforever.github.io/PSF-LoginServer/net/psforever/index.html)
|
||||
|
||||
## Tools
|
||||
|
||||
|
|
@ -225,22 +211,6 @@ psf-decode-packets -o ./output-directory foo.gcap bar.gcap
|
|||
By default, decodePackets takes in `.gcap` files, but it can also take gcapy ascii files with the
|
||||
`-p` option. Run `psf-decode-packets --help` to get usage info.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### Unable to initialize pscrypto
|
||||
|
||||
If you get an error like below
|
||||
|
||||
```
|
||||
12:17:28.037 [main] ERROR PsLogin - Unable to initialize pscrypto
|
||||
java.lang.UnsatisfiedLinkError: Unable to load library 'pscrypto': Native library (win32-x86-64/pscrypto.dll) not found in resource path
|
||||
```
|
||||
|
||||
Then you are missing the native library required to provide cryptographic functions to the login server. To fix this,
|
||||
you need a binary build of [PSCrypto](#downloading-pscrypto).
|
||||
|
||||
If you are still having trouble on Linux, try putting the library in `root directory/pscrypto-lib/libpscrypto.so`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please fork the project and provide a pull request to contribute code. Coding guidelines and contribution checklists
|
||||
|
|
|
|||
27
build.sbt
27
build.sbt
|
|
@ -49,12 +49,11 @@ lazy val psforeverSettings = Seq(
|
|||
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.9",
|
||||
"com.typesafe.akka" %% "akka-coordination" % "2.6.9",
|
||||
"com.typesafe.akka" %% "akka-cluster-tools" % "2.6.9",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.9",
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
|
||||
"org.specs2" %% "specs2-core" % "4.10.3" % "test",
|
||||
"org.scalatest" %% "scalatest" % "3.2.2" % "test",
|
||||
"org.scodec" %% "scodec-core" % "1.11.7",
|
||||
"net.java.dev.jna" % "jna" % "5.6.0",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.9",
|
||||
"ch.qos.logback" % "logback-classic" % "1.2.3",
|
||||
"org.log4s" %% "log4s" % "1.8.2",
|
||||
"org.fusesource.jansi" % "jansi" % "1.18",
|
||||
|
|
@ -78,18 +77,13 @@ lazy val psforeverSettings = Seq(
|
|||
"io.circe" %% "circe-core" % "0.13.0",
|
||||
"io.circe" %% "circe-generic" % "0.13.0",
|
||||
"io.circe" %% "circe-parser" % "0.13.0",
|
||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0"
|
||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0",
|
||||
"org.bouncycastle" % "bcprov-jdk15on" % "1.66"
|
||||
),
|
||||
// TODO(chord): remove exclusion when SessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279
|
||||
coverageExcludedPackages := "net\\.psforever\\.actors\\.session\\.SessionActor.*"
|
||||
)
|
||||
|
||||
lazy val pscryptoSettings = Seq(
|
||||
unmanagedClasspath in Test += (baseDirectory in ThisBuild).value / "pscrypto-lib",
|
||||
unmanagedClasspath in Runtime += (baseDirectory in ThisBuild).value / "pscrypto-lib",
|
||||
unmanagedClasspath in Compile += (baseDirectory in ThisBuild).value / "pscrypto-lib"
|
||||
)
|
||||
|
||||
lazy val psforever = (project in file("."))
|
||||
.configs(QuietTest)
|
||||
.settings(psforeverSettings: _*)
|
||||
|
|
@ -98,7 +92,6 @@ lazy val psforever = (project in file("."))
|
|||
// Copy all tests from Test -> QuietTest (we're only changing the run options)
|
||||
inConfig(QuietTest)(Defaults.testTasks)
|
||||
)
|
||||
.settings(pscryptoSettings: _*)
|
||||
|
||||
lazy val server = (project in file("server"))
|
||||
.configs(QuietTest)
|
||||
|
|
@ -113,24 +106,24 @@ lazy val server = (project in file("server"))
|
|||
packMain := Map("psforever-server" -> "net.psforever.server.Server"),
|
||||
packArchivePrefix := "psforever-server",
|
||||
packJvmOpts := Map("psforever-server" -> Seq("-Dstacktrace.app.packages=net.psforever")),
|
||||
packExtraClasspath := Map("psforever-server" -> Seq("${PROG_HOME}/pscrypto-lib", "${PROG_HOME}/config")),
|
||||
packResourceDir += (baseDirectory.in(psforever).value / "pscrypto-lib" -> "pscrypto-lib"),
|
||||
packResourceDir += (baseDirectory.in(psforever).value / "config" -> "config")
|
||||
packExtraClasspath := Map("psforever-server" -> Seq("${PROG_HOME}/config")),
|
||||
packResourceDir += (baseDirectory.in(psforever).value / "config" -> "config")
|
||||
)
|
||||
.settings(pscryptoSettings: _*)
|
||||
.dependsOn(psforever)
|
||||
|
||||
lazy val decodePackets = (project in file("tools/decode-packets"))
|
||||
.enablePlugins(PackPlugin)
|
||||
.settings(psforeverSettings: _*)
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0"
|
||||
),
|
||||
packMain := Map("psforever-decode-packets" -> "net.psforever.tools.decodePackets.DecodePackets")
|
||||
)
|
||||
.dependsOn(psforever)
|
||||
|
||||
lazy val client = (project in file("tools/client"))
|
||||
.enablePlugins(PackPlugin)
|
||||
.settings(psforeverSettings: _*)
|
||||
.dependsOn(psforever)
|
||||
|
||||
// Special test configuration for really quiet tests (used in CI)
|
||||
lazy val QuietTest = config("quiet") extend Test
|
||||
|
||||
|
|
|
|||
|
|
@ -10,15 +10,7 @@ services:
|
|||
ports:
|
||||
- 51000-51001:51000-51001/udp
|
||||
- 51002:51002/tcp
|
||||
command: >
|
||||
sh -c '
|
||||
if [ ! -d "pscrypto-lib" ]; then
|
||||
wget https://github.com/psforever/PSCrypto/releases/download/v1.1/pscrypto-lib-1.1.zip
|
||||
unzip pscrypto-lib-1.1.zip
|
||||
rm pscrypto-lib-1.1.zip
|
||||
fi
|
||||
sbt server/run
|
||||
'
|
||||
command: sbt server/run
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
sbt.version = 1.3.8
|
||||
sbt.version = 1.3.13
|
||||
|
|
|
|||
1
server/src/main/resources/sentry.properties
Normal file
1
server/src/main/resources/sentry.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
stacktrace.app.packages=net.psforever
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
package net.psforever.server
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
import java.nio.file.Paths
|
||||
import java.util.Locale
|
||||
import java.util.UUID.randomUUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import akka.routing.RandomPool
|
||||
import akka.{actor => classic}
|
||||
import ch.qos.logback.classic.LoggerContext
|
||||
import ch.qos.logback.classic.joran.JoranConfigurator
|
||||
import io.sentry.Sentry
|
||||
import kamon.Kamon
|
||||
import net.psforever.actors.net.{LoginActor, MiddlewareActor, SocketActor}
|
||||
import net.psforever.actors.session.SessionActor
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.login.psadmin.PsAdminActor
|
||||
import net.psforever.login._
|
||||
import net.psforever.objects.Default
|
||||
|
|
@ -33,6 +35,8 @@ import org.fusesource.jansi.Ansi.Color._
|
|||
import org.fusesource.jansi.Ansi._
|
||||
import org.slf4j
|
||||
import scopt.OParser
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.packet.PlanetSidePacket
|
||||
|
||||
object Server {
|
||||
private val logger = org.log4s.getLogger
|
||||
|
|
@ -90,38 +94,25 @@ object Server {
|
|||
implicit val system: ActorSystem = classic.ActorSystem("PsLogin")
|
||||
Default(system)
|
||||
|
||||
/** Create pipelines for the login and world servers
|
||||
*
|
||||
* The first node in the pipe is an Actor that handles the crypto for protecting packets.
|
||||
* After any crypto operations have been applied or unapplied, the packets are passed on to the next
|
||||
* actor in the chain. For an incoming packet, this is a player session handler. For an outgoing packet
|
||||
* this is the session router, which returns the packet to the sending host.
|
||||
*
|
||||
* See SessionRouter.scala for a diagram
|
||||
*/
|
||||
val loginTemplate = List(
|
||||
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]()),
|
||||
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]()),
|
||||
SessionPipeline("login-session-", classic.Props[LoginSessionActor]())
|
||||
)
|
||||
val worldTemplate = List(
|
||||
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]()),
|
||||
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]()),
|
||||
SessionPipeline("world-session-", classic.Props[SessionActor]())
|
||||
)
|
||||
|
||||
val netSim: Option[NetworkSimulatorParameters] = if (Config.app.development.netSim.enable) {
|
||||
val params = NetworkSimulatorParameters(
|
||||
Config.app.development.netSim.loss,
|
||||
Config.app.development.netSim.delay.toMillis,
|
||||
Config.app.development.netSim.reorderChance,
|
||||
Config.app.development.netSim.reorderTime.toMillis
|
||||
)
|
||||
logger.warn("NetSim is active")
|
||||
logger.warn(params.toString)
|
||||
Some(params)
|
||||
} else {
|
||||
None
|
||||
// typed to classic wrappers for login and session actors
|
||||
val login = (ref: ActorRef[MiddlewareActor.Command], connectionId: String) => {
|
||||
Behaviors.setup[PlanetSidePacket](context => {
|
||||
val actor = context.actorOf(classic.Props(new LoginActor(ref, connectionId)), "login")
|
||||
Behaviors.receiveMessage(message => {
|
||||
actor ! message
|
||||
Behaviors.same
|
||||
})
|
||||
})
|
||||
}
|
||||
val session = (ref: ActorRef[MiddlewareActor.Command], connectionId: String) => {
|
||||
Behaviors.setup[PlanetSidePacket](context => {
|
||||
val uuid = randomUUID().toString
|
||||
val actor = context.actorOf(classic.Props(new SessionActor(ref, connectionId)), s"session-${uuid}")
|
||||
Behaviors.receiveMessage(message => {
|
||||
actor ! message
|
||||
Behaviors.same
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
||||
|
|
@ -137,27 +128,19 @@ object Server {
|
|||
serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence")
|
||||
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager")
|
||||
|
||||
val loginRouter = classic.Props(new SessionRouter("Login", loginTemplate))
|
||||
val worldRouter = classic.Props(new SessionRouter("World", worldTemplate))
|
||||
val loginListener = system.actorOf(
|
||||
classic.Props(new UdpListener(loginRouter, "login-session-router", bindAddress, Config.app.login.port, netSim)),
|
||||
"login-udp-endpoint"
|
||||
)
|
||||
val worldListener = system.actorOf(
|
||||
classic.Props(new UdpListener(worldRouter, "world-session-router", bindAddress, Config.app.world.port, netSim)),
|
||||
"world-udp-endpoint"
|
||||
)
|
||||
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket")
|
||||
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")
|
||||
|
||||
val adminListener = system.actorOf(
|
||||
classic.Props(
|
||||
new TcpListener(
|
||||
classOf[PsAdminActor],
|
||||
"net.psforever.login.psadmin-client-",
|
||||
"psadmin-client-",
|
||||
InetAddress.getByName(Config.app.admin.bind),
|
||||
Config.app.admin.port
|
||||
)
|
||||
),
|
||||
"net.psforever.login.psadmin-tcp-endpoint"
|
||||
"psadmin-tcp-endpoint"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
@ -206,31 +189,6 @@ object Server {
|
|||
case Right(_) =>
|
||||
}
|
||||
|
||||
/** Initialize the PSCrypto native library
|
||||
*
|
||||
* PSCrypto provides PlanetSide specific crypto that is required to communicate with it.
|
||||
* It has to be distributed as a native library because there is no Scala version of the required
|
||||
* cryptographic primitives (MD5MAC). See https://github.com/psforever/PSCrypto for more information.
|
||||
*/
|
||||
try {
|
||||
CryptoInterface.initialize()
|
||||
} catch {
|
||||
case e: UnsatisfiedLinkError =>
|
||||
logger.error("Unable to initialize " + CryptoInterface.libName)
|
||||
logger.error(e)(
|
||||
"This means that your PSCrypto version is out of date. Get the latest version from the README" +
|
||||
" https://github.com/psforever/PSF-LoginServer#downloading-pscrypto"
|
||||
)
|
||||
sys.exit(1)
|
||||
case e: IllegalArgumentException =>
|
||||
logger.error("Unable to initialize " + CryptoInterface.libName)
|
||||
logger.error(e)(
|
||||
"This means that your PSCrypto version is out of date. Get the latest version from the README" +
|
||||
" https://github.com/psforever/PSF-LoginServer#downloading-pscrypto"
|
||||
)
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val builder = OParser.builder[CliConfig]
|
||||
|
||||
val parser = {
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package net.psforever.pslogin
|
||||
|
||||
import akka.actor.{ActorRef, MDCContextAware}
|
||||
import akka.testkit.TestProbe
|
||||
import net.psforever.login.HelloFriend
|
||||
import net.psforever.packet.{ControlPacket, GamePacket}
|
||||
|
||||
final case class MDCGamePacket(packet: GamePacket)
|
||||
|
||||
final case class MDCControlPacket(packet: ControlPacket)
|
||||
|
||||
class MDCTestProbe(probe: TestProbe) extends MDCContextAware {
|
||||
/*
|
||||
The way this test mediator works needs to be explained.
|
||||
|
||||
MDCContextAware objects initialize themselves in a chain of ActorRefs defined in the HelloFriend message.
|
||||
As the iterator is consumed, it produces a right-neighbor (r-neighbor) that is much further along the chain.
|
||||
The HelloFriend is passed to that r-neighbor and that is how subsequent neighbors are initialized and chained.
|
||||
|
||||
MDCContextAware objects consume and produce internal messages called MdcMsg that wrap around the payload.
|
||||
Normally inaccessible from the outside, the payload is unwrapped within the standard receive PartialFunction.
|
||||
By interacting with a TestProbe constructor param, information that would be concealed by MdcMsg can be polled.
|
||||
|
||||
The l-neighbor of the MDCContextAware is the system of the base.actor.base.ActorTest TestKit.
|
||||
The r-neighbor of the MDCContextAware is this MDCTestProbe and, indirectly, the TestProbe that was interjected.
|
||||
Pass l-input into the MDCContextAware itself.
|
||||
The r-output is a normal message that can be polled on that TestProbe.
|
||||
Pass r-input into this MDCTestProbe directly.
|
||||
The l-output is an MdcMsg that can be treated just as r-output, sending it to this Actor and polling the TestProbe.
|
||||
*/
|
||||
private var left: ActorRef = ActorRef.noSender
|
||||
|
||||
def receive: Receive = {
|
||||
case msg @ HelloFriend(_, _) =>
|
||||
left = sender()
|
||||
probe.ref ! msg
|
||||
|
||||
case MDCGamePacket(msg) =>
|
||||
left ! msg
|
||||
|
||||
case MDCControlPacket(msg) =>
|
||||
left ! msg
|
||||
|
||||
case msg =>
|
||||
left ! msg
|
||||
probe.ref ! msg
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,28 @@
|
|||
package net.psforever.pslogin
|
||||
/*
|
||||
|
||||
import actor.base.ActorTest
|
||||
import akka.actor.{ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import net.psforever.login.{HelloFriend, PacketCodingActor, RawPacket}
|
||||
import net.psforever.actors.net.MiddlewareActor
|
||||
import net.psforever.objects.avatar.Certification
|
||||
import net.psforever.packet.control.{ControlSync, MultiPacketBundle, SlottedMetaPacket}
|
||||
import net.psforever.packet.{ControlPacket, GamePacket, GamePacketOpcode, PacketCoding}
|
||||
import net.psforever.packet.control.{ControlSync, SlottedMetaPacket}
|
||||
import net.psforever.packet.{GamePacketOpcode, PacketCoding}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.objectcreate.ObjectClass
|
||||
import net.psforever.types._
|
||||
import scodec.bits._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class PacketCodingActor1Test extends ActorTest {
|
||||
"PacketCodingActor" should {
|
||||
"construct" in {
|
||||
system.actorOf(Props[PacketCodingActor](), "pca")
|
||||
system.actorOf(Props[MiddlewareActor](), "pca")
|
||||
//just construct without failing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PacketCodingActor2Test extends ActorTest {
|
||||
"PacketCodingActor" should {
|
||||
"initialize (no r-neighbor)" in {
|
||||
val pca: ActorRef = system.actorOf(Props[PacketCodingActor](), "pca")
|
||||
within(200 millis) {
|
||||
pca ! HelloFriend(135, List.empty[ActorRef].iterator)
|
||||
expectNoMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PacketCodingActor3Test extends ActorTest {
|
||||
"PacketCodingActor" should {
|
||||
"initialize (an r-neighbor)" in {
|
||||
val probe1 = TestProbe()
|
||||
val probe2 = system.actorOf(Props(classOf[MDCTestProbe], probe1), "mdc-probe")
|
||||
val pca: ActorRef = system.actorOf(Props[PacketCodingActor](), "pca")
|
||||
val iter = List(probe2).iterator
|
||||
val msg = HelloFriend(135, iter)
|
||||
|
||||
assert(iter.hasNext)
|
||||
pca ! msg
|
||||
probe1.expectMsg(msg) //pca will pass message directly; a new HelloFriend would be an unequal different object
|
||||
assert(!iter.hasNext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PacketCodingActor4Test extends ActorTest {
|
||||
val string_hex = RawPacket(hex"2A 9F05 D405 86")
|
||||
val string_obj = ObjectAttachMessage(PlanetSideGUID(1439), PlanetSideGUID(1492), 6)
|
||||
|
|
@ -577,7 +548,7 @@ class PacketCodingActorITest extends ActorTest {
|
|||
probe1.receiveOne(300 milli) match {
|
||||
case RawPacket(data) =>
|
||||
assert(data == string_hex)
|
||||
PacketCoding.DecodePacket(data).require match {
|
||||
PacketCoding.decodePacket(data).require match {
|
||||
case _: SlottedMetaPacket =>
|
||||
assert(true)
|
||||
case _ =>
|
||||
|
|
@ -916,3 +887,5 @@ class PacketCodingActorLTest extends ActorTest {
|
|||
object PacketCodingActorTest {
|
||||
//decoy
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,30 +1,50 @@
|
|||
package net.psforever.login
|
||||
package net.psforever.actors.net
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed}
|
||||
import com.github.t3hnar.bcrypt._
|
||||
import net.psforever.objects.{Account, Default}
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.{PlanetSideGamePacket, _}
|
||||
import net.psforever.persistence
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.util.Config
|
||||
import net.psforever.util.Database._
|
||||
import org.log4s.MDC
|
||||
import scodec.bits._
|
||||
import net.psforever.services.ServiceManager
|
||||
import net.psforever.services.ServiceManager.Lookup
|
||||
import net.psforever.services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.util.Config
|
||||
import net.psforever.util.Database._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
/*
|
||||
object LoginActor {
|
||||
def apply(
|
||||
middlewareActor: typed.ActorRef[MiddlewareActor.Command],
|
||||
uuid: String
|
||||
): Behavior[Command] =
|
||||
Behaviors.setup(context => new LoginActor(context, middlewareActor, uuid).start())
|
||||
|
||||
class LoginSessionActor extends Actor with MDCContextAware {
|
||||
sealed trait Command
|
||||
|
||||
}
|
||||
|
||||
class LoginActor(
|
||||
middlewareActor: typed.ActorRef[MiddlewareActor.Command],
|
||||
uuid: String
|
||||
) {
|
||||
|
||||
def start(): Unit = {
|
||||
Behaviors.receiveMessagePartial {}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String)
|
||||
extends Actor
|
||||
with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
@ -51,32 +71,14 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
// Reference: https://stackoverflow.com/a/50470009
|
||||
private val numBcryptPasses = 10
|
||||
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
|
||||
override def postStop() = {
|
||||
if (updateServerListTask != null)
|
||||
updateServerListTask.cancel()
|
||||
}
|
||||
|
||||
def receive = Initializing
|
||||
|
||||
def Initializing: Receive = {
|
||||
case HelloFriend(aSessionId, pipe) =>
|
||||
this.sessionId = aSessionId
|
||||
leftRef = sender()
|
||||
if (pipe.hasNext) {
|
||||
rightRef = pipe.next()
|
||||
rightRef !> HelloFriend(aSessionId, pipe)
|
||||
} else {
|
||||
rightRef = sender()
|
||||
}
|
||||
context.become(Started)
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
|
||||
case _ =>
|
||||
log.error("Unknown message")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
def Started: Receive = {
|
||||
def receive: Receive = {
|
||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||
accountIntermediary = endpoint
|
||||
case ReceiveIPAddress(address) =>
|
||||
|
|
@ -86,30 +88,11 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
port = address.Port
|
||||
case UpdateServerList() =>
|
||||
updateServerList()
|
||||
case ControlPacket(_, ctrl) =>
|
||||
handleControlPkt(ctrl)
|
||||
case GamePacket(_, _, game) =>
|
||||
handleGamePkt(game)
|
||||
case packet: PlanetSideGamePacket =>
|
||||
handleGamePkt(packet)
|
||||
case default => failWithError(s"Invalid packet class received: $default")
|
||||
}
|
||||
|
||||
def handleControlPkt(pkt: PlanetSideControlPacket) = {
|
||||
pkt match {
|
||||
/// 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, _, _, _, _, _, fa, fb) =>
|
||||
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, fa, fb, fb, fa)))
|
||||
|
||||
case TeardownConnection(_) =>
|
||||
sendResponse(DropSession(sessionId, "client requested session termination"))
|
||||
|
||||
case default =>
|
||||
log.error(s"Unhandled ControlPacket $default")
|
||||
}
|
||||
}
|
||||
|
||||
def handleGamePkt(pkt: PlanetSideGamePacket) =
|
||||
pkt match {
|
||||
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) =>
|
||||
|
|
@ -131,8 +114,8 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
|
||||
log.info(s"Connect to world request for '$name'")
|
||||
val response = ConnectToWorldMessage(serverName, publicAddress.getAddress.getHostAddress, publicAddress.getPort)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||
sendResponse(DropSession(sessionId, "user transferring to world"))
|
||||
middlewareActor ! MiddlewareActor.Send(response)
|
||||
middlewareActor ! MiddlewareActor.Close()
|
||||
|
||||
case _ =>
|
||||
log.debug(s"Unhandled GamePacket $pkt")
|
||||
|
|
@ -141,7 +124,6 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
def accountLogin(username: String, password: String): Unit = {
|
||||
import ctx._
|
||||
val newToken = this.generateToken()
|
||||
log.info("accountLogin")
|
||||
val result = for {
|
||||
// backwards compatibility: prefer exact match first, then try lowercase
|
||||
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username)))
|
||||
|
|
@ -153,26 +135,23 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
|
||||
case Some(account) => Future.successful(Some(account))
|
||||
case None => {
|
||||
Config.app.login.createMissingAccounts match {
|
||||
case true =>
|
||||
val passhash: String = password.bcrypt(numBcryptPasses)
|
||||
ctx.run(
|
||||
query[persistence.Account]
|
||||
.insert(_.passhash -> lift(passhash), _.username -> lift(username))
|
||||
.returningGenerated(_.id)
|
||||
) flatMap { id => ctx.run(query[persistence.Account].filter(_.id == lift(id))) } map { accounts =>
|
||||
Some(accounts.head)
|
||||
}
|
||||
case false =>
|
||||
loginFailureResponse(username, newToken)
|
||||
Future.successful(None)
|
||||
case None =>
|
||||
if (Config.app.login.createMissingAccounts) {
|
||||
val passhash: String = password.bcrypt(numBcryptPasses)
|
||||
ctx.run(
|
||||
query[persistence.Account]
|
||||
.insert(_.passhash -> lift(passhash), _.username -> lift(username))
|
||||
.returningGenerated(_.id)
|
||||
) flatMap { id => ctx.run(query[persistence.Account].filter(_.id == lift(id))) } map { accounts =>
|
||||
Some(accounts.head)
|
||||
}
|
||||
} else {
|
||||
loginFailureResponse(username, newToken)
|
||||
Future.successful(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
login <- accountOption match {
|
||||
case Some(account) =>
|
||||
log.info(s"$account")
|
||||
(account.inactive, password.isBcrypted(account.passhash)) match {
|
||||
case (false, true) =>
|
||||
accountIntermediary ! StoreAccountData(newToken, Account(account.id, account.username, account.gm))
|
||||
|
|
@ -187,7 +166,7 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
)
|
||||
loginSuccessfulResponse(username, newToken)
|
||||
updateServerListTask =
|
||||
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 2 seconds, self, UpdateServerList())
|
||||
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, UpdateServerList())
|
||||
future
|
||||
case (_, false) =>
|
||||
loginPwdFailureResponse(username, newToken)
|
||||
|
|
@ -202,77 +181,65 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
|
||||
result.onComplete {
|
||||
case Success(_) =>
|
||||
case Failure(e) => log.error(e.getMessage())
|
||||
case Failure(e) => log.error(e.getMessage)
|
||||
}
|
||||
}
|
||||
|
||||
def loginSuccessfulResponse(username: String, newToken: String) = {
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
middlewareActor ! MiddlewareActor.Send(
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.Success,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.Success,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
0,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginPwdFailureResponse(username: String, newToken: String) = {
|
||||
log.info(s"Failed login to account $username")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
middlewareActor ! MiddlewareActor.Send(
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginFailureResponse(username: String, newToken: String) = {
|
||||
log.info("DB problem")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.unk1,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
middlewareActor ! MiddlewareActor.Send(
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.unk1,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginAccountFailureResponse(username: String, newToken: String) = {
|
||||
log.info(s"Account $username inactive")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountClosed,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
middlewareActor ! MiddlewareActor.Send(
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountClosed,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -287,35 +254,25 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
|
||||
def updateServerList() = {
|
||||
val msg = VNLWorldStatusMessage(
|
||||
"Welcome to PlanetSide! ",
|
||||
Vector(
|
||||
WorldInformation(
|
||||
serverName,
|
||||
WorldStatus.Up,
|
||||
Config.app.world.serverType,
|
||||
Vector(WorldConnectionInfo(publicAddress)),
|
||||
PlanetSideEmpire.VS
|
||||
middlewareActor ! MiddlewareActor.Send(
|
||||
VNLWorldStatusMessage(
|
||||
"Welcome to PlanetSide! ",
|
||||
Vector(
|
||||
WorldInformation(
|
||||
serverName,
|
||||
WorldStatus.Up,
|
||||
Config.app.world.serverType,
|
||||
Vector(WorldConnectionInfo(publicAddress)),
|
||||
PlanetSideEmpire.VS
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, msg))
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
def failWithError(error: String): Unit = {
|
||||
log.error(error)
|
||||
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
||||
middlewareActor ! MiddlewareActor.Close()
|
||||
}
|
||||
|
||||
def sendResponse(cont: Any) = {
|
||||
log.trace("LOGIN SEND: " + cont)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> cont
|
||||
}
|
||||
|
||||
def sendRawResponse(pkt: ByteVector) = {
|
||||
log.trace("LOGIN SEND RAW: " + pkt)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> RawPacket(pkt)
|
||||
}
|
||||
}
|
||||
593
src/main/scala/net/psforever/actors/net/MiddlewareActor.scala
Normal file
593
src/main/scala/net/psforever/actors/net/MiddlewareActor.scala
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
package net.psforever.actors.net
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.security.{SecureRandom, Security}
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import akka.actor.typed.{ActorRef, ActorTags, Behavior, PostStop, Signal}
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
|
||||
import akka.io.Udp
|
||||
import net.psforever.packet.{
|
||||
CryptoPacketOpcode,
|
||||
PacketCoding,
|
||||
PlanetSideControlPacket,
|
||||
PlanetSideCryptoPacket,
|
||||
PlanetSideGamePacket,
|
||||
PlanetSidePacket
|
||||
}
|
||||
import net.psforever.packet.control.{
|
||||
ClientStart,
|
||||
ConnectionClose,
|
||||
ControlSync,
|
||||
ControlSyncResp,
|
||||
HandleGamePacket,
|
||||
MultiPacket,
|
||||
MultiPacketEx,
|
||||
RelatedA,
|
||||
RelatedB,
|
||||
ServerStart,
|
||||
SlottedMetaPacket,
|
||||
TeardownConnection
|
||||
}
|
||||
import net.psforever.packet.crypto.{ClientChallengeXchg, ClientFinished, ServerChallengeXchg, ServerFinished}
|
||||
import net.psforever.packet.game.{
|
||||
ChangeFireModeMessage,
|
||||
CharacterInfoMessage,
|
||||
KeepAliveMessage,
|
||||
ObjectCreateDetailedMessage,
|
||||
PingMsg
|
||||
}
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits.{BitVector, ByteVector, HexStringSyntax}
|
||||
import scodec.interop.akka.EnrichedByteVector
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import net.psforever.packet.PacketCoding.CryptoCoding
|
||||
import net.psforever.util.{DiffieHellman, Md5Mac}
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import scodec.Attempt
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** MiddlewareActor sits between the raw UDP socket and the "main" actors (either login or session) and handles
|
||||
* crypto and control packets. This means it sets up cryptography, it decodes incoming packets,
|
||||
* it encodes, bundles and splits outgoing packets, it handles things like requesting/resending lost packets and more.
|
||||
*/
|
||||
object MiddlewareActor {
|
||||
Security.addProvider(new BouncyCastleProvider)
|
||||
|
||||
/** Maximum packet size in bytes */
|
||||
//final val MTU: Int = 467
|
||||
final val MTU: Int = 440
|
||||
|
||||
def apply(
|
||||
socket: ActorRef[Udp.Command],
|
||||
sender: InetSocketAddress,
|
||||
next: (ActorRef[Command], String) => Behavior[PlanetSidePacket],
|
||||
connectionId: String
|
||||
): Behavior[Command] =
|
||||
Behaviors.setup(context => new MiddlewareActor(context, socket, sender, next, connectionId).start())
|
||||
|
||||
sealed trait Command
|
||||
|
||||
/** Receive incoming packet */
|
||||
final case class Receive(msg: ByteVector) extends Command
|
||||
|
||||
/** Send outgoing packet */
|
||||
final case class Send(msg: PlanetSidePacket) extends Command
|
||||
|
||||
/** Close connection */
|
||||
final case class Close() extends Command
|
||||
}
|
||||
|
||||
class MiddlewareActor(
|
||||
context: ActorContext[MiddlewareActor.Command],
|
||||
socket: ActorRef[Udp.Command],
|
||||
sender: InetSocketAddress,
|
||||
next: (ActorRef[MiddlewareActor.Command], String) => Behavior[PlanetSidePacket],
|
||||
connectionId: String
|
||||
) {
|
||||
|
||||
import MiddlewareActor._
|
||||
|
||||
implicit val ec: ExecutionContextExecutor = context.executionContext
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
var clientNonce: Long = 0
|
||||
|
||||
var serverMACBuffer: ByteVector = ByteVector.empty
|
||||
|
||||
val random = new SecureRandom()
|
||||
|
||||
var crypto: Option[CryptoCoding] = None
|
||||
|
||||
val nextActor: ActorRef[PlanetSidePacket] =
|
||||
context.spawnAnonymous(next(context.self, connectionId), ActorTags(s"id=${connectionId}"))
|
||||
|
||||
/** Queue of incoming packets (plus sequence numbers and timestamps) that arrived in the wrong order */
|
||||
val inReorderQueue: ListBuffer[(PlanetSidePacket, Int, Long)] = ListBuffer()
|
||||
|
||||
/** Latest incoming sequence number */
|
||||
var inSequence = 0
|
||||
|
||||
/** Latest incoming subslot number */
|
||||
var inSubslot = 0
|
||||
|
||||
/** List of missing subslot numbers and attempts counter */
|
||||
var inSubslotsMissing: mutable.Map[Int, Int] = mutable.Map()
|
||||
|
||||
/** Queue of outgoing packets used for bundling and splitting */
|
||||
val outQueue: mutable.Queue[(PlanetSidePacket, BitVector)] = mutable.Queue()
|
||||
|
||||
/** Queue of outgoing packets ready for sending */
|
||||
val outQueueBundled: mutable.Queue[PlanetSidePacket] = mutable.Queue()
|
||||
|
||||
/** Latest outgoing sequence number */
|
||||
var outSequence = 0
|
||||
|
||||
def nextSequence: Int = {
|
||||
val r = outSequence
|
||||
if (outSequence == 0xffff) {
|
||||
outSequence = 0
|
||||
} else {
|
||||
outSequence += 1
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/** Latest outgoing subslot number */
|
||||
var outSubslot = 0
|
||||
|
||||
def nextSubslot: Int = {
|
||||
val r = outSubslot
|
||||
if (outSubslot == 0xffff) {
|
||||
outSubslot = 0
|
||||
} else {
|
||||
outSubslot += 1
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/** Create a new SlottedMetaPacket with the sequence number filled in and the packet added to the history */
|
||||
def smp(slot: Int, data: ByteVector): SlottedMetaPacket = {
|
||||
if (outSlottedMetaPackets.length > 100) {
|
||||
outSlottedMetaPackets = outSlottedMetaPackets.takeRight(100)
|
||||
}
|
||||
val packet = SlottedMetaPacket(slot, nextSubslot, data)
|
||||
outSlottedMetaPackets += packet
|
||||
packet
|
||||
}
|
||||
|
||||
/** History of sent SlottedMetaPackets in case the client requests missing SMP packets via a RelatedA packet. */
|
||||
var outSlottedMetaPackets: ListBuffer[SlottedMetaPacket] = ListBuffer()
|
||||
|
||||
/** Timer that handles the bundling and throttling of outgoing packets and the reordering of incoming packets */
|
||||
val queueProcessor: Cancellable = {
|
||||
context.system.scheduler.scheduleWithFixedDelay(10.milliseconds, 10.milliseconds)(() => {
|
||||
try {
|
||||
if (outQueue.nonEmpty && outQueueBundled.isEmpty) {
|
||||
var length = 0L
|
||||
val bundle = outQueue
|
||||
.dequeueWhile {
|
||||
case (packet, payload) =>
|
||||
// packet length + MultiPacketEx prefix length
|
||||
val packetLength = payload.length + (if (payload.length < 256 * 8) { 1L * 8 }
|
||||
else if (payload.length < 65536 * 8) { 2L * 8 }
|
||||
else { 4L * 8 })
|
||||
length += packetLength
|
||||
|
||||
packet match {
|
||||
// Super awkward special case: Bundling CharacterInfoMessage with OCDM causes the character selection
|
||||
// to show blank lines and be broken. So we only dequeue either if they are the first packet.
|
||||
case _: CharacterInfoMessage | _: ObjectCreateDetailedMessage =>
|
||||
length == packetLength
|
||||
case _ =>
|
||||
// Some packets may be larger than the MTU limit, in that case we dequeue anyway and split later
|
||||
// We deduct some bytes to leave room for SlottedMetaPacket (4 bytes) and MultiPacketEx (2 bytes + prefix per packet)
|
||||
length == packetLength || length <= (MTU - 6) * 8
|
||||
}
|
||||
}
|
||||
.map(_._2)
|
||||
|
||||
if (bundle.length == 1) {
|
||||
outQueueBundled.enqueueAll(splitPacket(bundle.head))
|
||||
} else {
|
||||
PacketCoding.encodePacket(MultiPacketEx(bundle.toVector.map(_.bytes))) match {
|
||||
case Successful(data) => outQueueBundled.enqueue(smp(0, data.bytes))
|
||||
case Failure(cause) => log.error(cause.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outQueueBundled.dequeueFirst(_ => true) match {
|
||||
case Some(packet) => send(packet, Some(nextSequence), crypto)
|
||||
case None => ()
|
||||
}
|
||||
|
||||
if (inReorderQueue.nonEmpty) {
|
||||
var currentSequence = inSequence
|
||||
val currentTime = System.currentTimeMillis()
|
||||
inReorderQueue
|
||||
.sortBy(_._2)
|
||||
.dropWhile {
|
||||
case (_, sequence, time) =>
|
||||
// Forward packet if next in sequence order or older than 20ms
|
||||
if (sequence == currentSequence + 1 || currentTime - time > 20) {
|
||||
currentSequence += 1
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
.foreach {
|
||||
case (packet, sequence, _) =>
|
||||
if (sequence > inSequence) {
|
||||
inSequence = sequence
|
||||
}
|
||||
in(packet)
|
||||
}
|
||||
}
|
||||
|
||||
if (inSubslotsMissing.nonEmpty) {
|
||||
inSubslotsMissing.foreach {
|
||||
case (subslot, attempts) =>
|
||||
if (attempts <= 50) {
|
||||
// Slight hack to send RelatedA less frequently, might want to put this on a separate timer
|
||||
if (attempts % 10 == 0) send(RelatedA(0, subslot))
|
||||
inSubslotsMissing(subslot) += 1
|
||||
} else {
|
||||
log.warn(s"Requesting subslot '$subslot' from client failed")
|
||||
inSubslotsMissing.remove(subslot)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: Throwable => log.error(e)("Queue processing error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
def start(): Behavior[Command] = {
|
||||
Behaviors.receiveMessagePartial {
|
||||
case Receive(msg) =>
|
||||
PacketCoding.unmarshalPacket(msg) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case (ClientStart(nonce), _) =>
|
||||
clientNonce = nonce
|
||||
val serverNonce = Math.abs(random.nextInt())
|
||||
send(ServerStart(nonce, serverNonce), None, None)
|
||||
cryptoSetup()
|
||||
|
||||
// TODO ResetSequence
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $packet in init")
|
||||
Behaviors.same
|
||||
}
|
||||
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
|
||||
PacketCoding.decodePacket(msg) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case ping: PingMsg =>
|
||||
// reflect the packet back to the sender
|
||||
send(ping)
|
||||
Behaviors.same
|
||||
case _: ChangeFireModeMessage =>
|
||||
// ignore
|
||||
Behaviors.same
|
||||
case _ =>
|
||||
log.error(s"Unexpected non-crypto packet type $packet in start")
|
||||
Behaviors.same
|
||||
}
|
||||
case Failure(e) =>
|
||||
log.error(s"Could not decode packet in start: $e")
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
case other =>
|
||||
log.error(s"Invalid message '$other' received in start")
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def cryptoSetup(): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessagePartial[Command] {
|
||||
case Receive(msg) =>
|
||||
PacketCoding.unmarshalPacket(msg, None, CryptoPacketOpcode.ClientChallengeXchg) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case (ClientChallengeXchg(time, challenge, p, g), Some(_)) =>
|
||||
serverMACBuffer ++= msg.drop(3)
|
||||
|
||||
val dh = DiffieHellman(p.toArray, g.toArray)
|
||||
|
||||
val clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge)
|
||||
val serverTime = System.currentTimeMillis() / 1000L
|
||||
val randomChallenge = randomBytes(0xc)
|
||||
val serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge)
|
||||
|
||||
serverMACBuffer ++= send(
|
||||
ServerChallengeXchg(serverTime, randomChallenge, ByteVector.view(dh.publicKey))
|
||||
).drop(3)
|
||||
|
||||
cryptoFinish(dh, clientChallenge, serverChallenge)
|
||||
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $packet in cryptoSetup")
|
||||
stop()
|
||||
}
|
||||
case Failure(e) =>
|
||||
log.error(s"Could not decode packet in cryptoSetup: ${e}")
|
||||
stop()
|
||||
}
|
||||
case other =>
|
||||
log.error(s"Invalid message '$other' received in cryptoSetup")
|
||||
stop()
|
||||
}
|
||||
.receiveSignal(onSignal)
|
||||
}
|
||||
|
||||
def cryptoFinish(dh: DiffieHellman, clientChallenge: ByteVector, serverChallenge: ByteVector): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessagePartial[Command] {
|
||||
case Receive(msg) =>
|
||||
PacketCoding.unmarshalPacket(msg, None, CryptoPacketOpcode.ClientFinished) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case (ClientFinished(clientPubKey, _), Some(_)) =>
|
||||
serverMACBuffer ++= msg.drop(3)
|
||||
|
||||
val agreedKey = dh.agree(clientPubKey.toArray)
|
||||
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
|
||||
hex"00000000" ++ serverChallenge ++ hex"00000000"
|
||||
|
||||
val masterSecret = new Md5Mac(ByteVector.view(agreedKey)).updateFinal(agreedMessage)
|
||||
val mac = new Md5Mac(masterSecret)
|
||||
|
||||
// To do? verify client challenge. The code below has always been commented out, so it probably never
|
||||
// worked and it surely doesn't work now. The whole cryptography is flawed because
|
||||
// of the 128bit p values for DH, so implementing security features is probably not worth it.
|
||||
/*
|
||||
val clientChallengeExpanded = mac.updateFinal(
|
||||
ByteVector(
|
||||
"client finished".getBytes
|
||||
) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01",
|
||||
0xc
|
||||
)
|
||||
*/
|
||||
|
||||
val serverChallengeResult = mac
|
||||
.updateFinal(ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01", 0xc)
|
||||
|
||||
val encExpansion = ByteVector.view("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
val decExpansion = ByteVector.view("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
|
||||
val expandedEncKey = mac.updateFinal(encExpansion, 64)
|
||||
val expandedDecKey = mac.updateFinal(decExpansion, 64)
|
||||
|
||||
crypto = Some(
|
||||
CryptoCoding(
|
||||
new SecretKeySpec(expandedEncKey.take(20).toArray, "RC5"),
|
||||
new SecretKeySpec(expandedDecKey.take(20).toArray, "RC5"),
|
||||
expandedEncKey.slice(20, 36),
|
||||
expandedDecKey.slice(20, 36)
|
||||
)
|
||||
)
|
||||
|
||||
send(ServerFinished(serverChallengeResult))
|
||||
|
||||
active()
|
||||
|
||||
case other =>
|
||||
log.error(s"Unexpected packet '$other' in cryptoFinish")
|
||||
stop()
|
||||
}
|
||||
case Failure(e) =>
|
||||
log.error(s"Could not decode packet in cryptoFinish: $e")
|
||||
stop()
|
||||
}
|
||||
case other =>
|
||||
log.error(s"Invalid message '$other' received in cryptoFinish")
|
||||
stop()
|
||||
}
|
||||
.receiveSignal(onSignal)
|
||||
}
|
||||
|
||||
def active(): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessage[Command] {
|
||||
case Receive(msg) =>
|
||||
PacketCoding.unmarshalPacket(msg, crypto) match {
|
||||
case Successful((packet, Some(sequence))) =>
|
||||
if (sequence == inSequence + 1) {
|
||||
inSequence = sequence
|
||||
in(packet)
|
||||
} else {
|
||||
inReorderQueue.addOne((packet, sequence, System.currentTimeMillis()))
|
||||
}
|
||||
case Successful((packet, None)) => in(packet)
|
||||
case Failure(e) => log.error(s"Could not decode packet: ${e}")
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case Send(packet) =>
|
||||
out(packet)
|
||||
Behaviors.same
|
||||
|
||||
case Close() =>
|
||||
outQueue
|
||||
.dequeueAll(_ => true)
|
||||
.foreach(p => send(smp(0, p._2.bytes), Some(nextSequence), crypto))
|
||||
stop()
|
||||
}
|
||||
.receiveSignal(onSignal)
|
||||
}
|
||||
|
||||
val onSignal: PartialFunction[(ActorContext[Command], Signal), Behavior[Command]] = {
|
||||
case (_, PostStop) =>
|
||||
context.stop(nextActor)
|
||||
queueProcessor.cancel()
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
/** Handle incoming packet */
|
||||
def in(packet: PlanetSidePacket): Behavior[Command] = {
|
||||
packet match {
|
||||
case packet: PlanetSideGamePacket =>
|
||||
nextActor ! packet
|
||||
Behaviors.same
|
||||
|
||||
case packet: PlanetSideControlPacket =>
|
||||
packet match {
|
||||
case SlottedMetaPacket(slot, subslot, inner) =>
|
||||
if (subslot > inSubslot + 1) {
|
||||
((inSubslot + 1) until subslot).foreach(s => inSubslotsMissing.addOne((s, 0)))
|
||||
} else if (inSubslotsMissing.contains(subslot)) {
|
||||
inSubslotsMissing.remove(subslot)
|
||||
} else if (inSubslotsMissing.isEmpty) {
|
||||
send(RelatedB(slot, subslot))
|
||||
}
|
||||
if (subslot > inSubslot) {
|
||||
inSubslot = subslot
|
||||
}
|
||||
in(PacketCoding.decodePacket(inner))
|
||||
Behaviors.same
|
||||
|
||||
case MultiPacket(packets) =>
|
||||
packets.foreach(p => in(PacketCoding.decodePacket(p)))
|
||||
Behaviors.same
|
||||
|
||||
case MultiPacketEx(packets) =>
|
||||
packets.foreach(p => in(PacketCoding.decodePacket(p)))
|
||||
Behaviors.same
|
||||
|
||||
case RelatedA(slot, subslot) =>
|
||||
log.info(s"Client indicated a packet is missing prior to slot '$slot' and subslot '$subslot'")
|
||||
outSlottedMetaPackets.find(_.subslot == subslot - 1) match {
|
||||
case Some(packet) => outQueueBundled.enqueue(packet)
|
||||
case None => log.warn(s"Client requested unknown subslot '$subslot'")
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case RelatedB(_, subslot) =>
|
||||
outSlottedMetaPackets = outSlottedMetaPackets.filter(_.subslot > subslot)
|
||||
Behaviors.same
|
||||
|
||||
case ControlSync(diff, _, _, _, _, _, fa, fb) =>
|
||||
// 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
|
||||
|
||||
// Work around the 2038 problem
|
||||
// TODO can we just start at 0 again? what is this for?
|
||||
val serverTick = math.min(System.currentTimeMillis(), 4294967295L)
|
||||
val nextDiff = if (diff == 65535) 0 else diff + 1
|
||||
send(ControlSyncResp(nextDiff, serverTick, fa, fb, fb, fa))
|
||||
Behaviors.same
|
||||
|
||||
case ConnectionClose() =>
|
||||
Behaviors.stopped
|
||||
|
||||
case TeardownConnection(_) =>
|
||||
stop()
|
||||
|
||||
case ClientStart(_) =>
|
||||
start()
|
||||
|
||||
case other =>
|
||||
log.warn(s"Unhandled control packet '$other'")
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
case packet: PlanetSideCryptoPacket =>
|
||||
log.error(s"Unexpected crypto packet '$packet'")
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def in(packet: Attempt[PlanetSidePacket]): Unit = {
|
||||
packet match {
|
||||
case Successful(packet) => in(packet)
|
||||
case Failure(cause) => log.error(cause.message)
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle outgoing packet */
|
||||
def out(packet: PlanetSidePacket): Unit = {
|
||||
packet match {
|
||||
case packet: KeepAliveMessage =>
|
||||
send(packet)
|
||||
|
||||
case _ =>
|
||||
PacketCoding.encodePacket(packet) match {
|
||||
case Successful(payload) => outQueue.enqueue((packet, payload))
|
||||
case Failure(cause) => log.error(cause.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def send(packet: PlanetSideControlPacket): ByteVector = {
|
||||
send(packet, if (crypto.isDefined) Some(nextSequence) else None, crypto)
|
||||
}
|
||||
|
||||
def send(packet: PlanetSideCryptoPacket): ByteVector = {
|
||||
send(packet, Some(nextSequence), crypto)
|
||||
}
|
||||
|
||||
def send(packet: PlanetSideGamePacket): ByteVector = {
|
||||
send(packet, Some(nextSequence), crypto)
|
||||
}
|
||||
|
||||
def send(packet: PlanetSidePacket, sequence: Option[Int], crypto: Option[CryptoCoding]): ByteVector = {
|
||||
PacketCoding.marshalPacket(packet, sequence, crypto) match {
|
||||
case Successful(bits) =>
|
||||
val bytes = bits.toByteVector
|
||||
socket ! Udp.Send(bytes.toByteString, sender)
|
||||
bytes
|
||||
case Failure(e) =>
|
||||
log.error(s"Failed to encode packet ${packet.getClass.getName}: $e")
|
||||
ByteVector.empty
|
||||
}
|
||||
}
|
||||
|
||||
def randomBytes(amount: Int): ByteVector = {
|
||||
val array = Array.ofDim[Byte](amount)
|
||||
random.nextBytes(array)
|
||||
ByteVector.view(array)
|
||||
}
|
||||
|
||||
def stop(): Behavior[Command] = {
|
||||
send(ConnectionClose())
|
||||
Behaviors.stopped
|
||||
}
|
||||
|
||||
/** Split packet into multiple chunks (if necessary)
|
||||
* Split packets are wrapped in a HandleGamePacket and sent as SlottedMetaPacket4
|
||||
* The purpose of SlottedMetaPacket4 may or may not be to indicate a split packet
|
||||
*/
|
||||
def splitPacket(packet: BitVector): Seq[PlanetSideControlPacket] = {
|
||||
if (packet.length > (MTU - 4) * 8) {
|
||||
PacketCoding.encodePacket(HandleGamePacket(packet.bytes)) match {
|
||||
case Successful(data) =>
|
||||
data.grouped((MTU - 8) * 8).map(vec => smp(4, vec.bytes)).toSeq
|
||||
case Failure(cause) =>
|
||||
log.error(cause.message)
|
||||
Seq()
|
||||
}
|
||||
} else {
|
||||
Seq(smp(0, packet.bytes))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
226
src/main/scala/net/psforever/actors/net/SocketActor.scala
Normal file
226
src/main/scala/net/psforever/actors/net/SocketActor.scala
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package net.psforever.actors.net
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.security.SecureRandom
|
||||
import java.util.UUID.randomUUID
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import akka.{actor => classic}
|
||||
import akka.actor.typed.{ActorRef, ActorTags, Behavior, PostStop, Terminated}
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import akka.io.{IO, Udp}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.packet.PlanetSidePacket
|
||||
import net.psforever.util.Config
|
||||
import scodec.interop.akka.EnrichedByteString
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.concurrent.duration.{DurationDouble, DurationInt}
|
||||
import scala.util.Random
|
||||
|
||||
/** SocketActor creates a UDP socket, receives packets and forwards them to MiddlewareActor
|
||||
* There is only one SocketActor, but each connected client gets its own MiddlewareActor
|
||||
*/
|
||||
object SocketActor {
|
||||
def apply(
|
||||
address: InetSocketAddress,
|
||||
next: (ActorRef[MiddlewareActor.Command], String) => Behavior[PlanetSidePacket]
|
||||
): Behavior[Command] =
|
||||
Behaviors.setup(context => new SocketActor(context, address, next).start())
|
||||
|
||||
sealed trait Command
|
||||
|
||||
private final case class UdpCommandMessage(message: Udp.Command) extends Command
|
||||
private final case class UdpEventMessage(message: Udp.Event) extends Command
|
||||
private final case class UdpUnboundMessage(message: Udp.Unbound) extends Command
|
||||
private final case class Bound(socket: classic.ActorRef) extends Command
|
||||
private final case class StopChild(ref: ActorRef[MiddlewareActor.Command]) extends Command
|
||||
|
||||
// Typed actors cannot access sender but you can only get the socket that way
|
||||
private class SenderHack(ref: ActorRef[SocketActor.Command]) extends classic.Actor {
|
||||
def receive: Receive = {
|
||||
case Udp.Bound(_) =>
|
||||
ref ! Bound(sender())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO? This doesn't quite support all parameters of the old network simulator
|
||||
// Need to decide wheter they are necessary or not
|
||||
// https://github.com/psforever/PSF-LoginServer/blob/07f447c2344ab55d581317316c41571772ac2242/src/main/scala/net/psforever/login/UdpNetworkSimulator.scala
|
||||
private object NetworkSimulator {
|
||||
def apply(socketActor: ActorRef[SocketActor.Command]): Behavior[Udp.Message] =
|
||||
Behaviors.setup(context => new NetworkSimulator(context, socketActor))
|
||||
}
|
||||
|
||||
private class NetworkSimulator(context: ActorContext[Udp.Message], socketActor: ActorRef[SocketActor.Command])
|
||||
extends AbstractBehavior[Udp.Message](context) {
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
override def onMessage(message: Udp.Message): Behavior[Udp.Message] = {
|
||||
message match {
|
||||
case _: Udp.Received | _: Udp.Send =>
|
||||
simulate(message)
|
||||
Behaviors.same
|
||||
case other =>
|
||||
socketActor ! toSocket(message)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
def simulate(message: Udp.Message): Unit = {
|
||||
if (Random.nextDouble() > Config.app.development.netSim.loss) {
|
||||
if (Random.nextDouble() <= Config.app.development.netSim.reorderChance) {
|
||||
context.scheduleOnce(
|
||||
ThreadLocalRandom.current().nextDouble(0.01, 0.2).seconds,
|
||||
socketActor,
|
||||
toSocket(message)
|
||||
)
|
||||
} else {
|
||||
socketActor ! toSocket(message)
|
||||
}
|
||||
} else {
|
||||
log.info("Network simulator dropped packet")
|
||||
}
|
||||
}
|
||||
|
||||
def toSocket(message: Udp.Message): Command =
|
||||
message match {
|
||||
case message: Udp.Command => UdpCommandMessage(message)
|
||||
case message: Udp.Event => UdpEventMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SocketActor(
|
||||
context: ActorContext[SocketActor.Command],
|
||||
address: InetSocketAddress,
|
||||
next: (ActorRef[MiddlewareActor.Command], String) => Behavior[PlanetSidePacket]
|
||||
) {
|
||||
import SocketActor._
|
||||
import SocketActor.Command
|
||||
|
||||
implicit val ec: ExecutionContextExecutor = context.executionContext
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
val udpCommandAdapter: ActorRef[Udp.Command] =
|
||||
if (!Config.app.development.netSim.enable) {
|
||||
context.messageAdapter[Udp.Command](UdpCommandMessage)
|
||||
} else {
|
||||
context.spawnAnonymous(NetworkSimulator(context.self))
|
||||
}
|
||||
|
||||
val updEventAdapter: ActorRef[Udp.Event] =
|
||||
if (!Config.app.development.netSim.enable) {
|
||||
context.messageAdapter[Udp.Event](UdpEventMessage)
|
||||
} else {
|
||||
context.spawnAnonymous(NetworkSimulator(context.self))
|
||||
}
|
||||
|
||||
val updUnboundAdapter: ActorRef[Udp.Unbound] = context.messageAdapter[Udp.Unbound](UdpUnboundMessage)
|
||||
val senderHack: classic.ActorRef = context.actorOf(classic.Props(new SenderHack(context.self)))
|
||||
|
||||
IO(Udp)(context.system.classicSystem).tell(Udp.Bind(updEventAdapter.toClassic, address), senderHack)
|
||||
|
||||
val random = new SecureRandom()
|
||||
|
||||
val packetActors: mutable.Map[InetSocketAddress, ActorRef[MiddlewareActor.Command]] = mutable.Map()
|
||||
|
||||
val incomingTimes: mutable.Map[InetSocketAddress, Long] = mutable.Map()
|
||||
val outgoingTimes: mutable.Map[InetSocketAddress, Long] = mutable.Map()
|
||||
|
||||
val sessionReaper: Cancellable = context.system.scheduler.scheduleWithFixedDelay(0.seconds, 5.seconds)(() => {
|
||||
val now = System.currentTimeMillis()
|
||||
packetActors.keys.foreach(addr => {
|
||||
incomingTimes.get(addr) match {
|
||||
case Some(time) =>
|
||||
if (now - time > Config.app.network.session.inboundGraceTime.toMillis) {
|
||||
context.self ! StopChild(packetActors(addr))
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
outgoingTimes.get(addr) match {
|
||||
case Some(time) =>
|
||||
if (now - time > Config.app.network.session.outboundGraceTime.toMillis) {
|
||||
context.self ! StopChild(packetActors(addr))
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
def start(): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessagePartial[Command] {
|
||||
case Bound(socket) =>
|
||||
active(socket)
|
||||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
sessionReaper.cancel()
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
def active(socket: classic.ActorRef): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessagePartial[Command] {
|
||||
case UdpEventMessage(message) =>
|
||||
message match {
|
||||
case Udp.Bound(_) => Behaviors.same
|
||||
case Udp.Received(data, remote) =>
|
||||
incomingTimes(remote) = System.currentTimeMillis()
|
||||
val ref = packetActors.get(remote) match {
|
||||
case Some(ref) => ref
|
||||
case None =>
|
||||
val connectionId = randomUUID.toString
|
||||
val ref = context.spawn(
|
||||
MiddlewareActor(udpCommandAdapter, remote, next, connectionId),
|
||||
s"middleware-${connectionId}",
|
||||
ActorTags(s"uuid=${connectionId}")
|
||||
)
|
||||
context.watch(ref)
|
||||
packetActors(remote) = ref
|
||||
ref
|
||||
}
|
||||
ref ! MiddlewareActor.Receive(data.toByteVector)
|
||||
Behaviors.same
|
||||
|
||||
case _ =>
|
||||
Behaviors.same
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case UdpCommandMessage(message) =>
|
||||
message match {
|
||||
case Udp.Send(_, remote, _) =>
|
||||
outgoingTimes(remote) = System.currentTimeMillis()
|
||||
case _ => ()
|
||||
}
|
||||
socket ! message
|
||||
Behaviors.same
|
||||
|
||||
case UdpUnboundMessage(_) =>
|
||||
Behaviors.stopped
|
||||
|
||||
case StopChild(ref) =>
|
||||
context.stop(ref)
|
||||
Behaviors.same
|
||||
|
||||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
sessionReaper.cancel()
|
||||
Behaviors.same
|
||||
case (_, Terminated(ref)) =>
|
||||
packetActors.find(_._2 == ref) match {
|
||||
case Some((remote, _)) => packetActors.remove(remote)
|
||||
case None => log.warn(s"Received Terminated for unknown actor ${ref}")
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -866,7 +866,7 @@ class AvatarActor(
|
|||
Behaviors.same
|
||||
|
||||
case ConsumeStamina(stamina) =>
|
||||
assert(stamina > 0)
|
||||
assert(stamina > 0, s"consumed stamina must be larger than 0, but is: ${stamina}")
|
||||
consumeStamina(stamina)
|
||||
Behaviors.same
|
||||
|
||||
|
|
@ -1121,8 +1121,8 @@ class AvatarActor(
|
|||
val result = ctx.run(query[persistence.Avatar].filter(_.accountId == lift(account.id)))
|
||||
result.onComplete {
|
||||
case Success(avatars) =>
|
||||
val gen: AtomicInteger = new AtomicInteger(1)
|
||||
val converter: CharacterSelectConverter = new CharacterSelectConverter
|
||||
val gen = new AtomicInteger(1)
|
||||
val converter = new CharacterSelectConverter
|
||||
|
||||
avatars.filter(!_.deleted) foreach { a =>
|
||||
val secondsSinceLastLogin = Seconds.secondsBetween(a.lastLogin, LocalDateTime.now()).getSeconds
|
||||
|
|
@ -1200,7 +1200,7 @@ class AvatarActor(
|
|||
}
|
||||
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), finished = true, 0)
|
||||
CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0)
|
||||
)
|
||||
|
||||
case Failure(e) => log.error(e)("db failure")
|
||||
|
|
|
|||
|
|
@ -11,16 +11,15 @@ import net.psforever.objects.{Default, GlobalDefinitions, Player, Session}
|
|||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
|
||||
import net.psforever.objects.zones.Zoning
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
import net.psforever.util.{Config, PointOfInterest}
|
||||
import net.psforever.zones.Zones
|
||||
import net.psforever.services.chat.ChatService
|
||||
import net.psforever.services.chat.ChatService.ChatChannel
|
||||
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.concurrent.duration._
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
|
||||
object ChatActor {
|
||||
def apply(
|
||||
|
|
@ -272,9 +271,8 @@ class ChatActor(
|
|||
case Some(turret: FacilityTurret) if turret.isUpgrading =>
|
||||
WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None)
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))).packet
|
||||
)
|
||||
// FIXME we shouldn't do it like that
|
||||
sessionActor.toClassic ! RequestDestroyMessage(PlanetSideGUID(guid))
|
||||
}
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
|
||||
|
|
@ -353,7 +351,7 @@ class ChatActor(
|
|||
val ntu: Int = 900 + r.nextInt(100) - silo.NtuCapacitor
|
||||
silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu)
|
||||
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
@ -474,7 +472,7 @@ class ChatActor(
|
|||
)
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@@CMT_CAPTUREBASE_usage")
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,310 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.crypto
|
||||
|
||||
import com.sun.jna.ptr.IntByReference
|
||||
import net.psforever.IFinalizable
|
||||
import sna.Library
|
||||
import com.sun.jna.Pointer
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
object CryptoInterface {
|
||||
final val libName = "pscrypto"
|
||||
final val fullLibName = libName
|
||||
final val PSCRYPTO_VERSION_MAJOR = 1
|
||||
final val PSCRYPTO_VERSION_MINOR = 1
|
||||
|
||||
/**
|
||||
* NOTE: this is a single, global shared library for the entire server's crypto needs
|
||||
*
|
||||
* Unfortunately, access to this object didn't used to be synchronized. I noticed that
|
||||
* tests for this module were hanging ("arrive at a shared secret" & "must fail to agree on
|
||||
* a secret..."). This heisenbug was responsible for failed Travis test runs and developer
|
||||
* issues as well. Using Windows minidumps, I tracked the issue to a single thread deep in
|
||||
* pscrypto.dll. It appeared to be executing an EB FE instruction (on Intel x86 this is
|
||||
* `jmp $-2` or jump to self), which is an infinite loop. The stack trace made little to no
|
||||
* sense and after banging my head on the wall for many hours, I assumed that something deep
|
||||
* in CryptoPP, the libgcc libraries, or MSVC++ was the cause (or myself). Now all access to
|
||||
* pscrypto functions that allocate and deallocate memory (DH_Start, RC5_Init) are synchronized.
|
||||
* This *appears* to have fixed the problem.
|
||||
*/
|
||||
final val psLib = new Library(libName)
|
||||
|
||||
final val RC5_BLOCK_SIZE = 8
|
||||
final val MD5_MAC_SIZE = 16
|
||||
|
||||
val functionsList = List(
|
||||
"PSCrypto_Init",
|
||||
"PSCrypto_Get_Version",
|
||||
"PSCrypto_Version_String",
|
||||
"RC5_Init",
|
||||
"RC5_Encrypt",
|
||||
"RC5_Decrypt",
|
||||
"DH_Start",
|
||||
"DH_Start_Generate",
|
||||
"DH_Agree",
|
||||
"MD5_MAC",
|
||||
"Free_DH",
|
||||
"Free_RC5"
|
||||
)
|
||||
|
||||
/**
|
||||
* Used to initialize the crypto library at runtime. The version is checked and
|
||||
* all functions are mapped.
|
||||
*/
|
||||
def initialize(): Unit = {
|
||||
// preload all library functions for speed
|
||||
functionsList foreach psLib.prefetch
|
||||
|
||||
val libraryMajor = new IntByReference
|
||||
val libraryMinor = new IntByReference
|
||||
|
||||
psLib.PSCrypto_Get_Version(libraryMajor, libraryMinor)[Unit]
|
||||
|
||||
if (!psLib.PSCrypto_Init(PSCRYPTO_VERSION_MAJOR, PSCRYPTO_VERSION_MINOR)[Boolean]) {
|
||||
throw new IllegalArgumentException(
|
||||
s"Invalid PSCrypto library version ${libraryMajor.getValue}.${libraryMinor.getValue}. Expected " +
|
||||
s"$PSCRYPTO_VERSION_MAJOR.$PSCRYPTO_VERSION_MINOR"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for debugging object loading
|
||||
*/
|
||||
def printEnvironment(): Unit = {
|
||||
import java.io.File
|
||||
|
||||
val classpath = System.getProperty("java.class.path")
|
||||
val classpathEntries = classpath.split(File.pathSeparator)
|
||||
|
||||
val myLibraryPath = System.getProperty("user.dir")
|
||||
val jnaLibrary = System.getProperty("jna.library.path")
|
||||
val javaLibrary = System.getProperty("java.library.path")
|
||||
println("User dir: " + myLibraryPath)
|
||||
println("JNA Lib: " + jnaLibrary)
|
||||
println("Java Lib: " + javaLibrary)
|
||||
print("Classpath: ")
|
||||
classpathEntries.foreach(println)
|
||||
|
||||
println("Required data model: " + System.getProperty("sun.arch.data.model"))
|
||||
}
|
||||
|
||||
def MD5MAC(key: ByteVector, message: ByteVector, bytesWanted: Int): ByteVector = {
|
||||
val out = Array.ofDim[Byte](bytesWanted)
|
||||
|
||||
// WARNING BUG: the function must be cast to something (even if void) otherwise it doesnt work
|
||||
val ret = psLib.MD5_MAC(key.toArray, key.length, message.toArray, message.length, out, out.length)[Boolean]
|
||||
|
||||
if (!ret)
|
||||
throw new Exception("MD5MAC failed to process")
|
||||
|
||||
ByteVector(out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two Message Authentication Codes are the same in constant time,
|
||||
* preventing a timing attack for MAC forgery
|
||||
*
|
||||
* @param mac1 A MAC value
|
||||
* @param mac2 Another MAC value
|
||||
*/
|
||||
def verifyMAC(mac1: ByteVector, mac2: ByteVector): Boolean = {
|
||||
var okay = true
|
||||
|
||||
// prevent byte by byte guessing
|
||||
if (mac1.length != mac2.length)
|
||||
return false
|
||||
|
||||
for (i <- 0 until mac1.length.toInt) {
|
||||
okay = okay && mac1 { i } == mac2 { i }
|
||||
}
|
||||
|
||||
okay
|
||||
}
|
||||
|
||||
class CryptoDHState extends IFinalizable {
|
||||
var started = false
|
||||
// these types MUST be Arrays of bytes for JNA to work
|
||||
val privateKey = Array.ofDim[Byte](16)
|
||||
val publicKey = Array.ofDim[Byte](16)
|
||||
val p = Array.ofDim[Byte](16)
|
||||
val g = Array.ofDim[Byte](16)
|
||||
var dhHandle = Pointer.NULL
|
||||
|
||||
def start(modulus: ByteVector, generator: ByteVector): Unit = {
|
||||
assertNotClosed
|
||||
|
||||
if (started)
|
||||
throw new IllegalStateException("DH state has already been started")
|
||||
|
||||
psLib.synchronized {
|
||||
dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer]
|
||||
}
|
||||
|
||||
if (dhHandle == Pointer.NULL)
|
||||
throw new Exception("DH initialization failed!")
|
||||
|
||||
modulus.copyToArray(p, 0)
|
||||
generator.copyToArray(g, 0)
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
def start(): Unit = {
|
||||
assertNotClosed
|
||||
|
||||
if (started)
|
||||
throw new IllegalStateException("DH state has already been started")
|
||||
|
||||
psLib.synchronized {
|
||||
dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer]
|
||||
}
|
||||
|
||||
if (dhHandle == Pointer.NULL)
|
||||
throw new Exception("DH initialization failed!")
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
def agree(otherPublicKey: ByteVector) = {
|
||||
if (!started)
|
||||
throw new IllegalStateException("DH state has not been started")
|
||||
|
||||
val agreedValue = Array.ofDim[Byte](16)
|
||||
val agreed = psLib.DH_Agree(dhHandle, agreedValue, privateKey, otherPublicKey.toArray)[Boolean]
|
||||
|
||||
if (!agreed)
|
||||
throw new Exception("Failed to DH agree")
|
||||
|
||||
ByteVector.view(agreedValue)
|
||||
}
|
||||
|
||||
private def checkAndReturnView(array: Array[Byte]) = {
|
||||
if (!started)
|
||||
throw new IllegalStateException("DH state has not been started")
|
||||
|
||||
ByteVector.view(array)
|
||||
}
|
||||
|
||||
def getPrivateKey = {
|
||||
checkAndReturnView(privateKey)
|
||||
}
|
||||
|
||||
def getPublicKey = {
|
||||
checkAndReturnView(publicKey)
|
||||
}
|
||||
|
||||
def getModulus = {
|
||||
checkAndReturnView(p)
|
||||
}
|
||||
|
||||
def getGenerator = {
|
||||
checkAndReturnView(g)
|
||||
}
|
||||
|
||||
override def close = {
|
||||
if (started) {
|
||||
// TODO: zero private key material
|
||||
psLib.synchronized {
|
||||
psLib.Free_DH(dhHandle)[Unit]
|
||||
}
|
||||
started = false
|
||||
}
|
||||
|
||||
super.close
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoState(val decryptionKey: ByteVector, val encryptionKey: ByteVector) extends IFinalizable {
|
||||
// Note that the keys must be returned as primitive Arrays for JNA to work
|
||||
var encCryptoHandle: Pointer = Pointer.NULL
|
||||
var decCryptoHandle: Pointer = Pointer.NULL
|
||||
|
||||
psLib.synchronized {
|
||||
encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer]
|
||||
decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer]
|
||||
}
|
||||
|
||||
if (encCryptoHandle == Pointer.NULL)
|
||||
throw new Exception("Encryption initialization failed!")
|
||||
|
||||
if (decCryptoHandle == Pointer.NULL)
|
||||
throw new Exception("Decryption initialization failed!")
|
||||
|
||||
def encrypt(plaintext: ByteVector): ByteVector = {
|
||||
if (plaintext.length % RC5_BLOCK_SIZE != 0)
|
||||
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
|
||||
|
||||
val ciphertext = Array.ofDim[Byte](plaintext.length.toInt)
|
||||
|
||||
val ret = psLib.RC5_Encrypt(encCryptoHandle, plaintext.toArray, plaintext.length, ciphertext)[Boolean]
|
||||
|
||||
if (!ret)
|
||||
throw new Exception("Failed to encrypt plaintext")
|
||||
|
||||
ByteVector.view(ciphertext)
|
||||
}
|
||||
|
||||
def decrypt(ciphertext: ByteVector): ByteVector = {
|
||||
if (ciphertext.length % RC5_BLOCK_SIZE != 0)
|
||||
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
|
||||
|
||||
val plaintext = Array.ofDim[Byte](ciphertext.length.toInt)
|
||||
|
||||
val ret = psLib.RC5_Decrypt(decCryptoHandle, ciphertext.toArray, ciphertext.length, plaintext)[Boolean]
|
||||
|
||||
if (!ret)
|
||||
throw new Exception("Failed to decrypt ciphertext")
|
||||
|
||||
ByteVector.view(plaintext)
|
||||
}
|
||||
|
||||
override def close = {
|
||||
psLib.synchronized {
|
||||
psLib.Free_RC5(encCryptoHandle)[Unit]
|
||||
psLib.Free_RC5(decCryptoHandle)[Unit]
|
||||
}
|
||||
super.close
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoStateWithMAC(
|
||||
decryptionKey: ByteVector,
|
||||
encryptionKey: ByteVector,
|
||||
val decryptionMACKey: ByteVector,
|
||||
val encryptionMACKey: ByteVector
|
||||
) extends CryptoState(decryptionKey, encryptionKey) {
|
||||
|
||||
/**
|
||||
* Performs a MAC operation over the message. Used when encrypting packets
|
||||
*
|
||||
* @param message the input message
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macForEncrypt(message: ByteVector): ByteVector = {
|
||||
MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a MAC operation over the message. Used when verifying decrypted packets
|
||||
*
|
||||
* @param message the input message
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macForDecrypt(message: ByteVector): ByteVector = {
|
||||
MD5MAC(decryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
|
||||
/**
|
||||
* MACs the plaintext message, encrypts it, and then returns the encrypted message with the
|
||||
* MAC appended to the end.
|
||||
*
|
||||
* @param message Arbitrary set of bytes
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macAndEncrypt(message: ByteVector): ByteVector = {
|
||||
encrypt(message) ++ MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, MDCContextAware}
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.crypto._
|
||||
import net.psforever.packet.game.PingMsg
|
||||
import org.log4s.MDC
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
|
||||
sealed trait CryptoSessionAPI
|
||||
final case class DropCryptoSession() extends CryptoSessionAPI
|
||||
|
||||
/**
|
||||
* Actor that stores crypto state for a connection, appropriately encrypts and decrypts packets,
|
||||
* and passes packets along to the next hop once processed.
|
||||
*/
|
||||
class CryptoSessionActor extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
var sessionId: Long = 0
|
||||
var leftRef: ActorRef = ActorRef.noSender
|
||||
var rightRef: ActorRef = ActorRef.noSender
|
||||
|
||||
var cryptoDHState: Option[CryptoInterface.CryptoDHState] = None
|
||||
var cryptoState: Option[CryptoInterface.CryptoStateWithMAC] = None
|
||||
val random = new SecureRandom()
|
||||
|
||||
// crypto handshake state
|
||||
var serverChallenge = ByteVector.empty
|
||||
var serverChallengeResult = ByteVector.empty
|
||||
var serverMACBuffer = ByteVector.empty
|
||||
|
||||
var clientPublicKey = ByteVector.empty
|
||||
var clientChallenge = ByteVector.empty
|
||||
var clientChallengeResult = ByteVector.empty
|
||||
|
||||
var clientNonce: Long = 0
|
||||
var serverNonce: Long = 0
|
||||
|
||||
// Don't leak crypto object memory even on an exception
|
||||
override def postStop() = {
|
||||
cleanupCrypto()
|
||||
}
|
||||
|
||||
def receive = Initializing
|
||||
|
||||
def Initializing: Receive = {
|
||||
case HelloFriend(sharedSessionId, pipe) =>
|
||||
import MDCContextAware.Implicits._
|
||||
this.sessionId = sharedSessionId
|
||||
leftRef = sender()
|
||||
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)
|
||||
}
|
||||
|
||||
def NewClient: Receive = {
|
||||
case RawPacket(msg) =>
|
||||
PacketCoding.UnmarshalPacket(msg) match {
|
||||
case Successful(p) =>
|
||||
log.trace("Initializing -> NewClient")
|
||||
|
||||
p match {
|
||||
case ControlPacket(_, ClientStart(nonce)) =>
|
||||
clientNonce = nonce
|
||||
serverNonce = Math.abs(random.nextInt())
|
||||
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, serverNonce)))
|
||||
log.trace(s"ClientStart($nonce), $serverNonce")
|
||||
|
||||
context.become(CryptoExchange)
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $p in state NewClient")
|
||||
}
|
||||
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
|
||||
PacketCoding.DecodePacket(msg) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case ping @ PingMsg(_, _) =>
|
||||
// reflect the packet back to the sender
|
||||
sendResponse(ping)
|
||||
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")
|
||||
}
|
||||
|
||||
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(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))
|
||||
val sentPacket = sendResponse(packet)
|
||||
// save the sent packet a MAC check
|
||||
serverMACBuffer ++= sentPacket.drop(3)
|
||||
context.become(CryptoSetupFinishing)
|
||||
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $pkt in state CryptoExchange")
|
||||
}
|
||||
}
|
||||
case default =>
|
||||
log.error(s"Invalid message '$default' received in state CryptoExchange")
|
||||
}
|
||||
|
||||
def CryptoSetupFinishing: Receive = {
|
||||
case RawPacket(msg) =>
|
||||
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
|
||||
case Failure(e) => log.error("Could not decode packet in state CryptoSetupFinishing: " + e)
|
||||
case Successful(p) =>
|
||||
log.trace("CryptoExchange -> CryptoSetupFinishing")
|
||||
|
||||
p match {
|
||||
case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) =>
|
||||
clientPublicKey = clientPubKey
|
||||
clientChallengeResult = clientChalResult
|
||||
|
||||
// save the packet we got for a MAC check later
|
||||
serverMACBuffer ++= msg.drop(3)
|
||||
|
||||
val dh = cryptoDHState.get
|
||||
val agreedValue = dh.agree(clientPublicKey)
|
||||
|
||||
// we are now done with the DH crypto object
|
||||
dh.close
|
||||
|
||||
/*println("Agreed: " + agreedValue)
|
||||
println(s"Client challenge: $clientChallenge")*/
|
||||
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
|
||||
hex"00000000" ++ serverChallenge ++ hex"00000000"
|
||||
|
||||
//println("In message: " + agreedMessage)
|
||||
|
||||
val masterSecret = CryptoInterface.MD5MAC(agreedValue, agreedMessage, 20)
|
||||
|
||||
//println("Master secret: " + masterSecret)
|
||||
|
||||
serverChallengeResult = CryptoInterface.MD5MAC(
|
||||
masterSecret,
|
||||
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 decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
|
||||
val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
|
||||
/*println("DecExpansion: " + decExpansion)
|
||||
println("EncExpansion: " + encExpansion)*/
|
||||
|
||||
// expand the encryption and decryption keys
|
||||
// The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys
|
||||
val expandedDecKey =
|
||||
CryptoInterface.MD5MAC(masterSecret, decExpansion, 0x40) // this is what is visible in IDA
|
||||
|
||||
val expandedEncKey = CryptoInterface.MD5MAC(masterSecret, encExpansion, 0x40)
|
||||
|
||||
val decKey = expandedDecKey.take(20)
|
||||
val encKey = expandedEncKey.take(20)
|
||||
val decMACKey = expandedDecKey.drop(20).take(16)
|
||||
val encMACKey = expandedEncKey.drop(20).take(16)
|
||||
|
||||
/*println("**** DecKey: " + decKey)
|
||||
println("**** EncKey: " + encKey)
|
||||
println("**** DecMacKey: " + decMACKey)
|
||||
println("**** EncMacKey: " + encMACKey)*/
|
||||
|
||||
// spin up our encryption program
|
||||
cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey))
|
||||
|
||||
val packet = PacketCoding.CreateCryptoPacket(seq, ServerFinished(serverChallengeResult))
|
||||
|
||||
sendResponse(packet)
|
||||
|
||||
context.become(Established)
|
||||
case default => failWithError(s"Unexpected packet type $default in state CryptoSetupFinished")
|
||||
}
|
||||
}
|
||||
case default => failWithError(s"Invalid message '$default' received in state CryptoSetupFinished")
|
||||
}
|
||||
|
||||
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 { //from network-side
|
||||
PacketCoding.UnmarshalPacket(msg) match {
|
||||
case Successful(p) =>
|
||||
p match {
|
||||
case encPacket @ EncryptedPacket(_ /*seq*/, _) =>
|
||||
PacketCoding.decryptPacketData(cryptoState.get, encPacket) match {
|
||||
case Successful(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 Failure(e) =>
|
||||
log.error("Could not decode raw packet: " + e)
|
||||
}
|
||||
}
|
||||
//message to self?
|
||||
case api: CryptoSessionAPI =>
|
||||
api match {
|
||||
case DropCryptoSession() =>
|
||||
handleEstablishedPacket(
|
||||
sender(),
|
||||
PacketCoding.CreateControlPacket(TeardownConnection(clientNonce))
|
||||
)
|
||||
}
|
||||
//echo the session router? isn't that normally the leftRef?
|
||||
case sessionAPI: SessionRouterAPI =>
|
||||
leftRef !> sessionAPI
|
||||
//error
|
||||
case default =>
|
||||
failWithError(s"Invalid message '$default' received in state Established")
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
log.error(error)
|
||||
}
|
||||
|
||||
def cleanupCrypto() = {
|
||||
if (cryptoDHState.isDefined) {
|
||||
cryptoDHState.get.close
|
||||
cryptoDHState = None
|
||||
}
|
||||
|
||||
if (cryptoState.isDefined) {
|
||||
cryptoState.get.close
|
||||
cryptoState = None
|
||||
}
|
||||
}
|
||||
|
||||
def resetState(): Unit = {
|
||||
context.become(receive)
|
||||
|
||||
// reset the crypto primitives
|
||||
cleanupCrypto()
|
||||
|
||||
serverChallenge = ByteVector.empty
|
||||
serverChallengeResult = ByteVector.empty
|
||||
serverMACBuffer = ByteVector.empty
|
||||
clientPublicKey = ByteVector.empty
|
||||
clientChallenge = ByteVector.empty
|
||||
clientChallengeResult = ByteVector.empty
|
||||
}
|
||||
|
||||
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; 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")
|
||||
}
|
||||
}
|
||||
|
||||
def sendResponse(cont: PlanetSidePacketContainer): ByteVector = {
|
||||
log.trace("CRYPTO SEND: " + cont)
|
||||
val pkt = PacketCoding.MarshalPacket(cont)
|
||||
pkt match {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
def sendResponse(pkt: PlanetSideGamePacket): ByteVector = {
|
||||
log.trace("CRYPTO SEND GAME: " + pkt)
|
||||
val pktEncoded = PacketCoding.EncodePacket(pkt)
|
||||
pktEncoded match {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
def getRandBytes(amount: Int): ByteVector = {
|
||||
val array = Array.ofDim[Byte](amount)
|
||||
random.nextBytes(array)
|
||||
ByteVector.view(array)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import net.psforever.objects.Default
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control.{HandleGamePacket, _}
|
||||
import org.log4s.MDC
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* 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 subslotOutbound: Int = 0
|
||||
private var subslotInbound: Int = 0
|
||||
private var leftRef: ActorRef = ActorRef.noSender
|
||||
private var rightRef: ActorRef = ActorRef.noSender
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
/*
|
||||
Since the client can indicate missing packets when sending SlottedMetaPackets we should keep a history of them to resend to the client when requested with a RelatedA packet
|
||||
Since the subslot counter can wrap around, we need to use a LinkedHashMap to maintain the order packets are inserted, then we can drop older entries as required
|
||||
For example when a RelatedB packet arrives we can remove any entries to the left of the received ones without risking removing newer entries if the subslot counter wraps around back to 0
|
||||
*/
|
||||
private var slottedPacketLog: mutable.LinkedHashMap[Int, ByteVector] = mutable.LinkedHashMap()
|
||||
|
||||
// Due to the fact the client can send `RelatedA` packets out of order, we need to keep a buffer of which subslots arrived correctly, order them
|
||||
// and then act accordingly to send the missing subslot packet after a specified timeout
|
||||
private var relatedALog: ArrayBuffer[Int] = ArrayBuffer()
|
||||
private var relatedABufferTimeout: Cancellable = Default.Cancellable
|
||||
|
||||
def AddSlottedPacketToLog(subslot: Int, packet: ByteVector): Unit = {
|
||||
val log_limit = 500 // Number of SlottedMetaPackets to keep in history
|
||||
if (slottedPacketLog.size > log_limit) {
|
||||
slottedPacketLog = slottedPacketLog.drop(slottedPacketLog.size - log_limit)
|
||||
}
|
||||
|
||||
slottedPacketLog { subslot } = packet
|
||||
}
|
||||
|
||||
override def postStop() = {
|
||||
subslotOutbound = 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 PacketCodingActor.SubslotResend() => {
|
||||
log.trace(s"Subslot resend timeout reached, session: ${sessionId}")
|
||||
relatedABufferTimeout.cancel()
|
||||
log.trace(s"Client indicated successful subslots ${relatedALog.sortBy(x => x).mkString(" ")}")
|
||||
|
||||
// If a non-contiguous range of RelatedA packets were received we may need to send multiple missing packets, thus split the array into contiguous ranges
|
||||
val sorted_log = relatedALog.sortBy(x => x)
|
||||
|
||||
val split_logs: ArrayBuffer[ArrayBuffer[Int]] = new ArrayBuffer[ArrayBuffer[Int]]()
|
||||
var curr: ArrayBuffer[Int] = ArrayBuffer()
|
||||
for (i <- 0 to sorted_log.size - 1) {
|
||||
if (i == 0 || (sorted_log(i) != sorted_log(i - 1) + 1)) {
|
||||
curr = new ArrayBuffer()
|
||||
split_logs.append(curr)
|
||||
}
|
||||
curr.append(sorted_log(i))
|
||||
}
|
||||
|
||||
if (split_logs.size > 1) log.trace(s"Split successful subslots into ${split_logs.size} contiguous chunks")
|
||||
|
||||
for (range <- split_logs) {
|
||||
log.trace(s"Processing chunk ${range.mkString(" ")}")
|
||||
val first_accepted_subslot = range.min
|
||||
val missing_subslot = first_accepted_subslot - 1
|
||||
slottedPacketLog.get(missing_subslot) match {
|
||||
case Some(packet: ByteVector) =>
|
||||
log.info(s"Resending packet with subslot: $missing_subslot to session: ${sessionId}")
|
||||
sendResponseLeft(packet)
|
||||
case None =>
|
||||
log.error(s"Couldn't find packet with subslot: ${missing_subslot} to resend to session ${sessionId}.")
|
||||
}
|
||||
}
|
||||
|
||||
relatedALog.clear()
|
||||
}
|
||||
case RawPacket(msg) =>
|
||||
if (sender() == rightRef) { //from LSA, WSA, etc., to network - encode
|
||||
mtuLimit(msg)
|
||||
} else { //from network, to LSA, WSA, etc. - decode
|
||||
UnmarshalInnerPacket(msg, "a packet")
|
||||
}
|
||||
//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
|
||||
handlePacketContainer(ctrl) //sendResponseRight
|
||||
}
|
||||
//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)
|
||||
}
|
||||
//bundling packets into a SlottedMetaPacket0/MultiPacketEx
|
||||
case msg @ MultiPacketBundle(list) =>
|
||||
log.trace(s"BUNDLE PACKET REQUEST SEND, LEFT (always): $msg")
|
||||
handleBundlePacket(list)
|
||||
//etc
|
||||
case msg =>
|
||||
if (sender() == rightRef) {
|
||||
log.trace(s"BASE CASE PACKET SEND, LEFT: $msg")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
leftRef !> msg
|
||||
} else {
|
||||
log.trace(s"BASE CASE PACKET SEND, RIGHT: $msg")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (subslotOutbound == 65536) { //TODO what is the actual wrap number?
|
||||
subslotOutbound = 0
|
||||
subslotOutbound
|
||||
} else {
|
||||
val curr = subslotOutbound
|
||||
subslotOutbound += 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 subslot = Subslot
|
||||
PacketCoding.EncodePacket(SlottedMetaPacket(4, subslot, bvec)) match {
|
||||
case Successful(bdata) =>
|
||||
AddSlottedPacketToLog(subslot, bdata.toByteVector)
|
||||
sendResponseLeft(bdata.toByteVector)
|
||||
case f: Failure =>
|
||||
log.error(s"$f")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a `List` of packets and sequentially re-package the elements from the list into multiple container packets.<br>
|
||||
* <br>
|
||||
* The original packets are encoded then paired with their encoding lengths plus extra space to prefix the length.
|
||||
* Encodings from these pairs are drawn from the list until into buckets that fit a maximum byte stream length.
|
||||
* The size limitation on any bucket is the MTU limit.
|
||||
* less by the base sizes of `MultiPacketEx` (2) and of `SlottedMetaPacket` (4).
|
||||
* @param bundle the packets to be bundled
|
||||
*/
|
||||
def handleBundlePacket(bundle: List[PlanetSidePacket]): Unit = {
|
||||
val packets: List[ByteVector] = recursiveEncode(bundle.iterator)
|
||||
recursiveFillPacketBuckets(packets.iterator, PacketCodingActor.MTU_LIMIT_BYTES - 6)
|
||||
.foreach(list => {
|
||||
handleBundlePacket(list.toVector)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a `Vector` of encoded packets and re-package them.
|
||||
* The normal order is to package the elements of the vector into a `MultiPacketEx`.
|
||||
* If the vector only has one element, it will get packaged by itself in a `SlottedMetaPacket`.
|
||||
* If that one element risks being too big for the MTU, however, it will be handled off to be split.
|
||||
* Splitting should preserve `Subslot` ordering with the rest of the bundling.
|
||||
* @param vec a specific number of byte streams
|
||||
*/
|
||||
def handleBundlePacket(vec: Vector[ByteVector]): Unit = {
|
||||
if (vec.size == 1) {
|
||||
val elem = vec.head
|
||||
if (elem.length > PacketCodingActor.MTU_LIMIT_BYTES - 4) {
|
||||
handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(elem)))
|
||||
} else {
|
||||
handleBundlePacket(elem)
|
||||
}
|
||||
} else {
|
||||
PacketCoding.EncodePacket(MultiPacketEx(vec)) match {
|
||||
case Successful(bdata) =>
|
||||
handleBundlePacket(bdata.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"bundling failed on MultiPacketEx creation: - $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept `ByteVector` data and package it into a `SlottedMetaPacket`.
|
||||
* Send it (towards the network) upon successful encoding.
|
||||
* @param data an encoded packet
|
||||
*/
|
||||
def handleBundlePacket(data: ByteVector): Unit = {
|
||||
val subslot = Subslot
|
||||
PacketCoding.EncodePacket(SlottedMetaPacket(0, subslot, data)) match {
|
||||
case Successful(bdata) =>
|
||||
AddSlottedPacketToLog(subslot, bdata.toByteVector)
|
||||
sendResponseLeft(bdata.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"bundling failed on SlottedMetaPacket creation: - $e")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform data into a container packet and re-submit that container to the process that handles the packet.
|
||||
* @param data the packet data
|
||||
* @param description an explanation of the input `data`
|
||||
*/
|
||||
def UnmarshalInnerPacket(data: ByteVector, description: String): Unit = {
|
||||
PacketCoding.unmarshalPayload(0, data) match { //TODO is it safe for this to always be 0?
|
||||
case Successful(packet) =>
|
||||
handlePacketContainer(packet)
|
||||
case Failure(ex) =>
|
||||
log.info(s"Failed to unmarshal $description: $ex. Data : $data")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort and redirect a container packet bound for the server by type of contents.
|
||||
* `GamePacket` objects can just onwards without issue.
|
||||
* `ControlPacket` objects may need to be dequeued.
|
||||
* All other container types are invalid.
|
||||
* @param container the container packet
|
||||
*/
|
||||
def handlePacketContainer(container: PlanetSidePacketContainer): Unit = {
|
||||
container match {
|
||||
case _: GamePacket =>
|
||||
sendResponseRight(container)
|
||||
case ControlPacket(_, ctrlPkt) =>
|
||||
handleControlPacket(container, ctrlPkt)
|
||||
case default =>
|
||||
log.warn(s"Invalid packet container class received: ${default.getClass.getName}") //do not spill contents in log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a control packet or determine that it does not need to be processed at this level.
|
||||
* Primarily, if the packet is of a type that contains another packet that needs be be unmarshalled,
|
||||
* that/those packet must be unwound.<br>
|
||||
* <br>
|
||||
* The subslot information is used to identify these nested packets after arriving at their destination,
|
||||
* to establish order for sequential packets and relation between divided packets.
|
||||
* @param container the original container packet
|
||||
* @param packet the packet that was extracted from the container
|
||||
*/
|
||||
def handleControlPacket(container: PlanetSidePacketContainer, packet: PlanetSideControlPacket) = {
|
||||
packet match {
|
||||
case SlottedMetaPacket(slot, subslot, innerPacket) =>
|
||||
subslotInbound = subslot
|
||||
self.tell(PacketCoding.CreateControlPacket(RelatedB(slot, subslot)), rightRef) //will go to the network
|
||||
UnmarshalInnerPacket(innerPacket, "the inner packet of a SlottedMetaPacket")
|
||||
|
||||
case MultiPacket(packets) =>
|
||||
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacket") }
|
||||
|
||||
case MultiPacketEx(packets) =>
|
||||
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacketEx") }
|
||||
|
||||
case RelatedA(slot, subslot) =>
|
||||
log.trace(s"Client indicated a packet is missing prior to slot: $slot subslot: $subslot, session: ${sessionId}")
|
||||
|
||||
relatedALog += subslot
|
||||
|
||||
// (re)start the timeout period, if no more RelatedA packets are sent before the timeout period elapses the missing packet(s) will be resent
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
relatedABufferTimeout.cancel()
|
||||
relatedABufferTimeout =
|
||||
context.system.scheduler.scheduleOnce(100 milliseconds, self, PacketCodingActor.SubslotResend())
|
||||
|
||||
case RelatedB(slot, subslot) =>
|
||||
log.trace(s"result $slot: subslot $subslot accepted, session: ${sessionId}")
|
||||
|
||||
// The client has indicated it's received up to a certain subslot, that means we can purge the log of any subslots prior to and including the confirmed subslot
|
||||
// Find where this subslot is stored in the packet log (if at all) and drop anything to the left of it, including itself
|
||||
if (relatedABufferTimeout.isCancelled || relatedABufferTimeout == Default.Cancellable) {
|
||||
val pos = slottedPacketLog.keySet.toArray.indexOf(subslot)
|
||||
if (pos != -1) {
|
||||
slottedPacketLog = slottedPacketLog.drop(pos + 1)
|
||||
log.trace(s"Subslots left in log: ${slottedPacketLog.keySet.toString()}")
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
sendResponseRight(container)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a series of packets and transform it into a series of packet encodings.
|
||||
* Packets that do not encode properly are simply excluded from the product.
|
||||
* This is not treated as an error or exception; a warning will merely be logged.
|
||||
* @param iter the `Iterator` for a series of packets
|
||||
* @param out updated series of byte stream data produced through successful packet encoding;
|
||||
* defaults to an empty list
|
||||
* @return a series of byte stream data produced through successful packet encoding
|
||||
*/
|
||||
@tailrec private def recursiveEncode(
|
||||
iter: Iterator[PlanetSidePacket],
|
||||
out: List[ByteVector] = List()
|
||||
): List[ByteVector] = {
|
||||
if (!iter.hasNext) {
|
||||
out
|
||||
} else {
|
||||
import net.psforever.packet.{PlanetSideControlPacket, PlanetSideGamePacket}
|
||||
iter.next() match {
|
||||
case msg: PlanetSideGamePacket =>
|
||||
PacketCoding.EncodePacket(msg) match {
|
||||
case Successful(bytecode) =>
|
||||
recursiveEncode(iter, out :+ bytecode.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"game packet $msg, part of a bundle, did not encode - $e")
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
case msg: PlanetSideControlPacket =>
|
||||
PacketCoding.EncodePacket(msg) match {
|
||||
case Successful(bytecode) =>
|
||||
recursiveEncode(iter, out :+ bytecode.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"control packet $msg, part of a bundle, did not encode - $e")
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
case _ =>
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a series of byte stream data and sort into sequential size-limited buckets of the same byte streams.
|
||||
* Note that elements that exceed `lim` by themselves are always sorted into their own buckets.
|
||||
* @param iter an `Iterator` of a series of byte stream data
|
||||
* @param lim the maximum stream length permitted
|
||||
* @param curr the stream length of the current bucket
|
||||
* @param out updated series of byte stream data stored in buckets
|
||||
* @return a series of byte stream data stored in buckets
|
||||
*/
|
||||
@tailrec private def recursiveFillPacketBuckets(
|
||||
iter: Iterator[ByteVector],
|
||||
lim: Int,
|
||||
curr: Int = 0,
|
||||
out: List[mutable.ListBuffer[ByteVector]] = List(mutable.ListBuffer())
|
||||
): List[mutable.ListBuffer[ByteVector]] = {
|
||||
if (!iter.hasNext) {
|
||||
out
|
||||
} else {
|
||||
val data = iter.next()
|
||||
var len = data.length.toInt
|
||||
len = len + (if (len < 256) { 1 }
|
||||
else if (len < 65536) { 2 }
|
||||
else { 4 }) //space for the prefixed length byte(s)
|
||||
if (curr + len > lim && out.last.nonEmpty) { //bucket must have something in it before swapping
|
||||
recursiveFillPacketBuckets(iter, lim, len, out :+ mutable.ListBuffer(data))
|
||||
} else {
|
||||
out.last += data
|
||||
recursiveFillPacketBuckets(iter, lim, curr + len, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PacketCodingActor {
|
||||
final val MTU_LIMIT_BYTES: Int = 467
|
||||
|
||||
private final case class SubslotResend()
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{ActorContext, ActorRef, PoisonPill, _}
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import scodec.bits._
|
||||
|
||||
sealed trait SessionState
|
||||
final case class New() extends SessionState
|
||||
final case class Related() extends SessionState
|
||||
final case class Handshaking() extends SessionState
|
||||
final case class Established() extends SessionState
|
||||
final case class Closing() extends SessionState
|
||||
final case class Closed() extends SessionState
|
||||
|
||||
class Session(
|
||||
val sessionId: Long,
|
||||
val socketAddress: InetSocketAddress,
|
||||
returnActor: ActorRef,
|
||||
sessionPipeline: List[SessionPipeline]
|
||||
)(
|
||||
implicit val context: ActorContext,
|
||||
implicit val self: ActorRef
|
||||
) {
|
||||
|
||||
var state: SessionState = New()
|
||||
val sessionCreatedTime: DateTime = DateTime.now()
|
||||
var sessionEndedTime: DateTime = DateTime.now()
|
||||
|
||||
val pipeline = sessionPipeline.map { actor =>
|
||||
val a = context.actorOf(actor.props, actor.nameTemplate + sessionId.toString)
|
||||
context.watch(a)
|
||||
a
|
||||
}
|
||||
|
||||
val pipelineIter = pipeline.iterator
|
||||
if (pipelineIter.hasNext) {
|
||||
pipelineIter.next() ! HelloFriend(sessionId, pipelineIter)
|
||||
}
|
||||
|
||||
// statistics
|
||||
var bytesSent: Long = 0
|
||||
var bytesReceived: Long = 0
|
||||
var inboundPackets: Long = 0
|
||||
var outboundPackets: Long = 0
|
||||
|
||||
var lastInboundEvent: Long = System.nanoTime()
|
||||
var lastOutboundEvent: Long = System.nanoTime()
|
||||
|
||||
var inboundPacketRate: Double = 0.0
|
||||
var outboundPacketRate: Double = 0.0
|
||||
var inboundBytesPerSecond: Double = 0.0
|
||||
var outboundBytesPerSecond: Double = 0.0
|
||||
|
||||
def receive(packet: RawPacket): Unit = {
|
||||
bytesReceived += packet.data.size
|
||||
inboundPackets += 1
|
||||
lastInboundEvent = System.nanoTime()
|
||||
|
||||
pipeline.head !> packet
|
||||
}
|
||||
|
||||
def send(packet: ByteVector): Unit = {
|
||||
bytesSent += packet.size
|
||||
outboundPackets += 1
|
||||
lastOutboundEvent = System.nanoTime()
|
||||
|
||||
returnActor ! SendPacket(packet, socketAddress)
|
||||
}
|
||||
|
||||
def dropSession(graceful: Boolean) = {
|
||||
pipeline.foreach(context.unwatch)
|
||||
pipeline.foreach(_ ! PoisonPill)
|
||||
|
||||
sessionEndedTime = DateTime.now()
|
||||
setState(Closed())
|
||||
}
|
||||
|
||||
def getState = state
|
||||
|
||||
def setState(newState: SessionState): Unit = {
|
||||
state = newState
|
||||
}
|
||||
def getPipeline: List[ActorRef] = pipeline
|
||||
|
||||
def getTotalBytes = {
|
||||
bytesSent + bytesReceived
|
||||
}
|
||||
|
||||
def timeSinceLastInboundEvent = {
|
||||
(System.nanoTime() - lastInboundEvent) / 1000000
|
||||
}
|
||||
|
||||
def timeSinceLastOutboundEvent = {
|
||||
(System.nanoTime() - lastOutboundEvent) / 1000000
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
s"Session($sessionId, $getTotalBytes)"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor._
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.control.ConnectionClose
|
||||
import net.psforever.util.Config
|
||||
import org.log4s.MDC
|
||||
import scodec.bits._
|
||||
import net.psforever.services.ServiceManager
|
||||
import net.psforever.services.ServiceManager.Lookup
|
||||
import net.psforever.services.account.{IPAddress, StoreIPAddress}
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
||||
sealed trait SessionRouterAPI
|
||||
|
||||
final case class RawPacket(data: ByteVector) extends SessionRouterAPI
|
||||
|
||||
final case class ResponsePacket(data: ByteVector) extends SessionRouterAPI
|
||||
|
||||
final case class DropSession(id: Long, reason: String) extends SessionRouterAPI
|
||||
|
||||
final case class SessionReaper() extends SessionRouterAPI
|
||||
|
||||
case class SessionPipeline(nameTemplate: String, props: Props)
|
||||
|
||||
/**
|
||||
* Login sessions are divided between two actors. The crypto session actor transparently handles all of the cryptographic
|
||||
* setup of the connection. Once a correct crypto session has been established, all packets, after being decrypted
|
||||
* will be passed on to the login session actor. This actor has important state that is used to maintain the login
|
||||
* session.
|
||||
*
|
||||
* > PlanetSide Session Pipeline <
|
||||
*
|
||||
* read() route decrypt
|
||||
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
|
||||
* /|\ | /|\ | /|\ |
|
||||
* | write() | | encrypt | | response |
|
||||
* +--------------+ +-----------+ +-----------------+
|
||||
*/
|
||||
class SessionRouter(role: String, pipeline: List[SessionPipeline]) extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val sessionReaper = context.system.scheduler.scheduleWithFixedDelay(10 seconds, 5 seconds, self, SessionReaper())
|
||||
|
||||
val idBySocket = mutable.Map[InetSocketAddress, Long]()
|
||||
val sessionById = mutable.Map[Long, Session]()
|
||||
val sessionByActor = mutable.Map[ActorRef, Session]()
|
||||
val closePacket = PacketCoding.EncodePacket(ConnectionClose()).require.bytes
|
||||
var accountIntermediary: ActorRef = ActorRef.noSender
|
||||
|
||||
var sessionId = 0L // this is a connection session, not an actual logged in session ID
|
||||
var inputRef: ActorRef = ActorRef.noSender
|
||||
|
||||
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
||||
|
||||
override def preStart() = {
|
||||
log.info(s"SessionRouter (for ${role}s) initializing ...")
|
||||
}
|
||||
|
||||
def receive = initializing
|
||||
|
||||
def initializing: Receive = {
|
||||
case Hello() =>
|
||||
inputRef = sender()
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||
accountIntermediary = endpoint
|
||||
log.info(s"SessionRouter starting; ready for $role sessions")
|
||||
context.become(started)
|
||||
case default =>
|
||||
log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
override def postStop() = {
|
||||
sessionReaper.cancel()
|
||||
}
|
||||
|
||||
def started: Receive = {
|
||||
case _ @ReceivedPacket(msg, from) =>
|
||||
var session: Session = null
|
||||
|
||||
if (!idBySocket.contains(from)) {
|
||||
session = createNewSession(from)
|
||||
} else {
|
||||
val id = idBySocket { from }
|
||||
session = sessionById { id }
|
||||
}
|
||||
|
||||
if (session.state != Closed()) {
|
||||
MDC("sessionId") = session.sessionId.toString
|
||||
log.trace(s"RECV: $msg -> ${session.getPipeline.head.path.name}")
|
||||
session.receive(RawPacket(msg))
|
||||
MDC.clear()
|
||||
}
|
||||
case ResponsePacket(msg) =>
|
||||
val session = sessionByActor.get(sender())
|
||||
|
||||
if (session.isDefined) {
|
||||
if (session.get.state != Closed()) {
|
||||
MDC("sessionId") = session.get.sessionId.toString
|
||||
log.trace(s"SEND: $msg -> ${inputRef.path.name}")
|
||||
session.get.send(msg)
|
||||
MDC.clear()
|
||||
}
|
||||
} else {
|
||||
log.error("Dropped old response packet from actor " + sender().path.name)
|
||||
}
|
||||
case DropSession(id, reason) =>
|
||||
val session = sessionById.get(id)
|
||||
|
||||
if (session.isDefined) {
|
||||
removeSessionById(id, reason, graceful = true)
|
||||
} else {
|
||||
log.error(s"Requested to drop non-existent session ID=$id from ${sender()}")
|
||||
}
|
||||
case SessionReaper() =>
|
||||
val inboundGrace = Config.app.network.session.inboundGraceTime.toMillis
|
||||
val outboundGrace = Config.app.network.session.outboundGraceTime.toMillis
|
||||
|
||||
sessionById.foreach {
|
||||
case (id, session) =>
|
||||
log.trace(session.toString)
|
||||
if (session.getState == Closed()) {
|
||||
// clear mappings
|
||||
session.getPipeline.foreach(sessionByActor remove)
|
||||
sessionById.remove(id)
|
||||
idBySocket.remove(session.socketAddress)
|
||||
log.debug(s"Reaped session ID=$id")
|
||||
} else if (session.timeSinceLastInboundEvent > inboundGrace) {
|
||||
removeSessionById(id, "session timed out (inbound)", graceful = false)
|
||||
} else if (session.timeSinceLastOutboundEvent > outboundGrace) {
|
||||
removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU
|
||||
}
|
||||
}
|
||||
case Terminated(actor) =>
|
||||
val terminatedSession = sessionByActor.get(actor)
|
||||
|
||||
if (terminatedSession.isDefined) {
|
||||
removeSessionById(terminatedSession.get.sessionId, s"${actor.path.name} died", graceful = true)
|
||||
} else {
|
||||
log.error("Received an invalid actor Termination from " + actor.path.name)
|
||||
}
|
||||
case default =>
|
||||
log.error(s"Unknown message $default from " + sender().path)
|
||||
}
|
||||
|
||||
def createNewSession(address: InetSocketAddress) = {
|
||||
val id = newSessionId
|
||||
val session = new Session(id, address, inputRef, pipeline)
|
||||
|
||||
// establish mappings for easy lookup
|
||||
idBySocket { address } = id
|
||||
sessionById { id } = session
|
||||
|
||||
session.getPipeline.foreach { actor =>
|
||||
sessionByActor { actor } = session
|
||||
}
|
||||
|
||||
log.info(s"New session ID=$id from " + address.toString)
|
||||
|
||||
if (role == "Login") {
|
||||
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
|
||||
def removeSessionById(id: Long, reason: String, graceful: Boolean): Unit = {
|
||||
val sessionOption = sessionById.get(id)
|
||||
|
||||
if (sessionOption.isEmpty)
|
||||
return
|
||||
|
||||
val session: Session = sessionOption.get
|
||||
|
||||
if (graceful) {
|
||||
for (_ <- 0 to 5) {
|
||||
session.send(closePacket)
|
||||
}
|
||||
}
|
||||
|
||||
// kill all session specific actors
|
||||
session.dropSession(graceful)
|
||||
log.info(s"Dropping session ID=$id (reason: $reason)")
|
||||
}
|
||||
|
||||
def newSessionId = {
|
||||
val oldId = sessionId
|
||||
sessionId += 1
|
||||
oldId
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, Terminated}
|
||||
import akka.io._
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
|
||||
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: Iterator[ActorRef])
|
||||
|
||||
class UdpListener(
|
||||
nextActorProps: Props,
|
||||
nextActorName: String,
|
||||
listenAddress: InetAddress,
|
||||
port: Int,
|
||||
netParams: Option[NetworkSimulatorParameters]
|
||||
) extends Actor {
|
||||
private val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def supervisorStrategy =
|
||||
OneForOneStrategy() {
|
||||
case _ => Stop
|
||||
}
|
||||
|
||||
import context.system
|
||||
|
||||
// If we have network parameters, start the network simulator
|
||||
if (netParams.isDefined) {
|
||||
// See http://www.cakesolutions.net/teamblogs/understanding-akkas-recommended-practice-for-actor-creation-in-scala
|
||||
// For why we cant do Props(new Actor) here
|
||||
val sim = context.actorOf(Props(classOf[UdpNetworkSimulator], self, netParams.get))
|
||||
IO(Udp).tell(Udp.Bind(sim, new InetSocketAddress(listenAddress, port)), sim)
|
||||
} else {
|
||||
IO(Udp) ! Udp.Bind(self, new InetSocketAddress(listenAddress, port))
|
||||
}
|
||||
|
||||
var bytesRecevied = 0L
|
||||
var bytesSent = 0L
|
||||
var nextActor: ActorRef = ActorRef.noSender
|
||||
|
||||
def receive = {
|
||||
case Udp.Bound(local) =>
|
||||
log.info(s"Now listening on UDP:$local")
|
||||
|
||||
createNextActor()
|
||||
context.become(ready(sender()))
|
||||
case Udp.CommandFailed(Udp.Bind(_, address, _)) =>
|
||||
log.error("Failed to bind to the network interface: " + address)
|
||||
context.system.terminate()
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
def ready(socket: ActorRef): Receive = {
|
||||
case SendPacket(msg, to) =>
|
||||
bytesSent += msg.size
|
||||
socket ! Udp.Send(msg.toByteString, to)
|
||||
case Udp.Received(data, remote) =>
|
||||
bytesRecevied += data.size
|
||||
nextActor ! ReceivedPacket(data.toByteVector, remote)
|
||||
case Udp.Unbind => socket ! Udp.Unbind
|
||||
case Udp.Unbound => context.stop(self)
|
||||
case Terminated(actor) =>
|
||||
log.error(s"Next actor ${actor.path.name} has died...restarting")
|
||||
createNextActor()
|
||||
case default => log.error(s"Unhandled message: $default")
|
||||
}
|
||||
|
||||
def createNextActor() = {
|
||||
nextActor = context.actorOf(nextActorProps, nextActorName)
|
||||
context.watch(nextActor)
|
||||
nextActor ! Hello()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package net.psforever.login
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import akka.io._
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/** Parameters for the Network simulator
|
||||
*
|
||||
* @param packetLoss The percentage from [0.0, 1.0] that a packet will be lost
|
||||
* @param packetDelay The end-to-end delay (ping) of all packets
|
||||
* @param packetReorderingChance The percentage from [0.0, 1.0] that a packet will be reordered
|
||||
* @param packetReorderingTime The absolute adjustment in milliseconds that a packet can have (either
|
||||
* forward or backwards in time)
|
||||
*/
|
||||
case class NetworkSimulatorParameters(
|
||||
packetLoss: Double,
|
||||
packetDelay: Long,
|
||||
packetReorderingChance: Double,
|
||||
packetReorderingTime: Long
|
||||
) {
|
||||
assert(packetLoss >= 0.0 && packetLoss <= 1.0)
|
||||
assert(packetDelay >= 0)
|
||||
assert(packetReorderingChance >= 0.0 && packetReorderingChance <= 1.0)
|
||||
assert(packetReorderingTime >= 0)
|
||||
|
||||
override def toString =
|
||||
"NetSimParams: loss %.2f%% / delay %dms / reorder %.2f%% / reorder +/- %dms".format(
|
||||
packetLoss * 100,
|
||||
packetDelay,
|
||||
packetReorderingChance * 100,
|
||||
packetReorderingTime
|
||||
)
|
||||
}
|
||||
|
||||
class UdpNetworkSimulator(server: ActorRef, params: NetworkSimulatorParameters) extends Actor {
|
||||
private val log = org.log4s.getLogger
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
//******* Variables
|
||||
val packetDelayDuration = (params.packetDelay / 2).milliseconds
|
||||
|
||||
type QueueItem = (Udp.Message, Long)
|
||||
|
||||
// sort in ascending order (older things get dequeued first)
|
||||
implicit val QueueItem = Ordering.by[QueueItem, Long](_._2).reverse
|
||||
|
||||
val inPacketQueue = mutable.PriorityQueue[QueueItem]()
|
||||
val outPacketQueue = mutable.PriorityQueue[QueueItem]()
|
||||
|
||||
val chaos = new Random()
|
||||
var interface = ActorRef.noSender
|
||||
|
||||
def receive = {
|
||||
case UdpNetworkSimulator.ProcessInputQueue() =>
|
||||
val time = System.nanoTime()
|
||||
var exit = false
|
||||
|
||||
while (inPacketQueue.nonEmpty && !exit) {
|
||||
val lastTime = time - inPacketQueue.head._2
|
||||
|
||||
// this packet needs to be sent within 20 milliseconds or more
|
||||
if (lastTime >= 20000000) {
|
||||
server.tell(inPacketQueue.dequeue()._1, interface)
|
||||
} else {
|
||||
schedule(lastTime.nanoseconds, outbound = false)
|
||||
exit = true
|
||||
}
|
||||
}
|
||||
case UdpNetworkSimulator.ProcessOutputQueue() =>
|
||||
val time = System.nanoTime()
|
||||
var exit = false
|
||||
|
||||
while (outPacketQueue.nonEmpty && !exit) {
|
||||
val lastTime = time - outPacketQueue.head._2
|
||||
|
||||
// this packet needs to be sent within 20 milliseconds or more
|
||||
if (lastTime >= 20000000) {
|
||||
interface.tell(outPacketQueue.dequeue()._1, server)
|
||||
} else {
|
||||
schedule(lastTime.nanoseconds, outbound = true)
|
||||
exit = true
|
||||
}
|
||||
}
|
||||
// outbound messages
|
||||
case msg @ Udp.Send(payload, target, _) =>
|
||||
handlePacket(msg, outPacketQueue, outbound = true)
|
||||
// inbound messages
|
||||
case msg @ Udp.Received(payload, sender) =>
|
||||
handlePacket(msg, inPacketQueue, outbound = false)
|
||||
case msg @ Udp.Bound(address) =>
|
||||
interface = sender()
|
||||
log.info(s"Hooked ${server.path} for network simulation")
|
||||
server.tell(msg, self) // make sure the server sends *us* the packets
|
||||
case default =>
|
||||
val from = sender()
|
||||
|
||||
if (from == server)
|
||||
interface.tell(default, server)
|
||||
else if (from == interface)
|
||||
server.tell(default, interface)
|
||||
else
|
||||
log.error("Unexpected sending Actor " + from.path)
|
||||
}
|
||||
|
||||
def handlePacket(message: Udp.Message, queue: mutable.PriorityQueue[QueueItem], outbound: Boolean) = {
|
||||
val name: String = if (outbound) "OUT" else "IN"
|
||||
val queue: mutable.PriorityQueue[QueueItem] = if (outbound) outPacketQueue else inPacketQueue
|
||||
|
||||
if (chaos.nextDouble() > params.packetLoss) {
|
||||
// if the message queue is empty, then we need to reschedule our task
|
||||
if (queue.isEmpty)
|
||||
schedule(packetDelayDuration, outbound)
|
||||
|
||||
// perform a reordering
|
||||
if (chaos.nextDouble() <= params.packetReorderingChance) {
|
||||
// creates the range (-1.0, 1.0)
|
||||
// time adjustment to move the packet (forward or backwards in time)
|
||||
val adj = (2 * (chaos.nextDouble() - 0.5) * params.packetReorderingTime).toLong
|
||||
queue += ((message, System.nanoTime() + adj * 1000000))
|
||||
|
||||
log.debug(s"Reordered $name by ${adj}ms - $message")
|
||||
} else { // normal message
|
||||
queue += ((message, System.nanoTime()))
|
||||
}
|
||||
} else {
|
||||
log.debug(s"Dropped $name - $message")
|
||||
}
|
||||
}
|
||||
|
||||
def schedule(duration: FiniteDuration, outbound: Boolean) =
|
||||
context.system.scheduler.scheduleOnce(
|
||||
packetDelayDuration,
|
||||
self,
|
||||
if (outbound) UdpNetworkSimulator.ProcessOutputQueue() else UdpNetworkSimulator.ProcessInputQueue()
|
||||
)
|
||||
}
|
||||
|
||||
object UdpNetworkSimulator {
|
||||
//******* Internal messages
|
||||
private final case class ProcessInputQueue()
|
||||
private final case class ProcessOutputQueue()
|
||||
}
|
||||
|
|
@ -93,6 +93,9 @@ class PsAdminActor(peerAddress: InetSocketAddress, connection: ActorRef) extends
|
|||
case Tcp.PeerClosed =>
|
||||
context.stop(self)
|
||||
|
||||
case Tcp.ErrorClosed(_) =>
|
||||
context.stop(self)
|
||||
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ sealed trait DeferrableMsg extends ContainableMsg
|
|||
|
||||
/**
|
||||
* A mixin for handling synchronized movement of `Equipment` items into or out from `Container` entities.
|
||||
* The most important feature of this synchronization is the movmement of equipment
|
||||
* The most important feature of this synchronization is the movemement of equipment
|
||||
* out from one container into another container
|
||||
* without causing representation overlap, overwriting, or unintended stacking of other equipment
|
||||
* including equipment that has nort yet been inserted.
|
||||
* including equipment that has not yet been inserted.
|
||||
*/
|
||||
trait ContainableBehavior {
|
||||
_: Actor =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet
|
||||
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, DecodeResult, Err}
|
||||
|
||||
// this isnt actually used as an opcode (i.e not serialized)
|
||||
// This isn't actually used as an opcode (i.e not serialized)
|
||||
object CryptoPacketOpcode extends Enumeration {
|
||||
type Type = Value
|
||||
val Ignore, ClientChallengeXchg, ServerChallengeXchg, ClientFinished, ServerFinished = Value
|
||||
|
|
|
|||
|
|
@ -1,246 +1,156 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet
|
||||
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import java.security.{Key, SecureRandom, Security}
|
||||
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.RC5ParameterSpec
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
import scodec.{Attempt, Codec, Err}
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err}
|
||||
import scodec.codecs.{bytes, uint16L, uint8L}
|
||||
|
||||
/**
|
||||
* Base trait of the packet container `case class`.
|
||||
*/
|
||||
sealed trait PlanetSidePacketContainer
|
||||
|
||||
/**
|
||||
* An encrypted packet contains the following:
|
||||
* a sequence;
|
||||
* an encrypted opcode;
|
||||
* an encrypted payload;
|
||||
* and an implicit MD5MAC plus padding.
|
||||
* @param sequenceNumber na
|
||||
* @param payload the packet data
|
||||
*/
|
||||
final case class EncryptedPacket(sequenceNumber: Int, payload: ByteVector) extends PlanetSidePacketContainer
|
||||
|
||||
/**
|
||||
* A crypto packet contains the following:
|
||||
* a sequence;
|
||||
* and, a payload.
|
||||
* These packets have no opcodes and they rely on implicit state to decode properly.
|
||||
* @param sequenceNumber na
|
||||
* @param packet the packet data
|
||||
*/
|
||||
final case class CryptoPacket(sequenceNumber: Int, packet: PlanetSideCryptoPacket) extends PlanetSidePacketContainer
|
||||
|
||||
/**
|
||||
* A game packet is prefaced by a byte that determines the type of packet and how to interpret the data.
|
||||
* This is important for decoding and encoding.
|
||||
* @param opcode a byte that identifies the packet
|
||||
* @param sequenceNumber na
|
||||
* @param packet the packet data
|
||||
*/
|
||||
final case class GamePacket(opcode: GamePacketOpcode.Value, sequenceNumber: Int, packet: PlanetSideGamePacket)
|
||||
extends PlanetSidePacketContainer
|
||||
|
||||
/**
|
||||
* A control packet is prefaced with a zero'd byte (`00`) followed by a special byte opcode for the type of control packet.
|
||||
* @param opcode a byte that identifies the packet
|
||||
* @param packet the packet data
|
||||
*/
|
||||
final case class ControlPacket(opcode: ControlPacketOpcode.Value, packet: PlanetSideControlPacket)
|
||||
extends PlanetSidePacketContainer
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import scodec.bits.ByteVector
|
||||
import net.psforever.util.Md5Mac
|
||||
|
||||
object PacketCoding {
|
||||
Security.addProvider(new BouncyCastleProvider)
|
||||
|
||||
/**
|
||||
* Access to the `ControlPacket` constructor.
|
||||
* @param packet a `PlanetSideControlPacket`
|
||||
* @return a `ControlPacket`
|
||||
*/
|
||||
def CreateControlPacket(packet: PlanetSideControlPacket) = ControlPacket(packet.opcode, packet)
|
||||
private val random = new SecureRandom()
|
||||
|
||||
/**
|
||||
* Access to the `CryptoPacket` constructor.
|
||||
* @param sequence na
|
||||
* @param packet a `PlanetSideCryptoPacket`
|
||||
* @return a `CryptoPacket`
|
||||
*/
|
||||
def CreateCryptoPacket(sequence: Int, packet: PlanetSideCryptoPacket) = CryptoPacket(sequence, packet)
|
||||
val RC5_BLOCK_SIZE = 8
|
||||
|
||||
/**
|
||||
* Access to the `GamePacket` constructor.
|
||||
* @param sequence na
|
||||
* @param packet a `PlanetSideGamePacket`
|
||||
* @return a `GamePacket`
|
||||
*/
|
||||
def CreateGamePacket(sequence: Int, packet: PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet)
|
||||
|
||||
/* Marshalling and Encoding. */
|
||||
/** A lower bound on the packet size */
|
||||
final val PLANETSIDE_MIN_PACKET_SIZE = 1
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param sequence the packet's sequence number. Must be set for all non ControlPacket packets (but always for encrypted packets).
|
||||
* @param crypto if set, encrypt final payload
|
||||
* @return a `BitVector` translated from the packet's data
|
||||
*/
|
||||
def MarshalPacket(packet: PlanetSidePacketContainer): Attempt[BitVector] = {
|
||||
var flagsEncoded: BitVector = BitVector.empty //flags before everything in packet
|
||||
var seqEncoded: BitVector = BitVector.empty //control packets have a sequence number
|
||||
var paddingEncoded: BitVector = BitVector.empty //encrypted packets need to be aligned in a certain way
|
||||
var payloadEncoded: BitVector = BitVector.empty //the packet itself as bits and bytes
|
||||
var controlPacket = false
|
||||
var sequenceNum = 0
|
||||
//packet flags
|
||||
var secured = false
|
||||
var packetType = PacketType.Crypto
|
||||
|
||||
packet match {
|
||||
case GamePacket(_, seq, payload) =>
|
||||
packetType = PacketType.Normal
|
||||
sequenceNum = seq
|
||||
EncodePacket(payload) match {
|
||||
case f @ Failure(_) => return f
|
||||
case Successful(p) => payloadEncoded = p
|
||||
def marshalPacket(
|
||||
packet: PlanetSidePacket,
|
||||
sequence: Option[Int] = None,
|
||||
crypto: Option[CryptoCoding] = None
|
||||
): Attempt[BitVector] = {
|
||||
val seq = packet match {
|
||||
case _: PlanetSideControlPacket if crypto.isEmpty => BitVector.empty
|
||||
case _ =>
|
||||
sequence match {
|
||||
case Some(sequence) =>
|
||||
uint16L.encode(sequence) match {
|
||||
case Successful(seq) => seq
|
||||
case f @ Failure(_) => return f
|
||||
}
|
||||
case None =>
|
||||
return Failure(Err(s"Missing sequence"))
|
||||
}
|
||||
}
|
||||
|
||||
case ControlPacket(_, payload) =>
|
||||
controlPacket = true
|
||||
EncodePacket(payload) match {
|
||||
val (flags, payload) = packet match {
|
||||
case _: PlanetSideGamePacket | _: PlanetSideControlPacket if crypto.isDefined =>
|
||||
encodePacket(packet) match {
|
||||
case Successful(payload) =>
|
||||
val encryptedPayload = crypto.get.encrypt(payload.bytes) match {
|
||||
case Successful(p) => p
|
||||
case f: Failure => return f
|
||||
}
|
||||
(
|
||||
PlanetSidePacketFlags.codec.encode(PlanetSidePacketFlags(PacketType.Normal, secured = true)).require,
|
||||
// encrypted packets need to be aligned to 4 bytes before encryption/decryption
|
||||
// first byte are flags, second is the sequence, and third is the pad
|
||||
hex"00".bits ++ encryptedPayload.bits
|
||||
)
|
||||
case f @ Failure(_) => return f
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
|
||||
case CryptoPacket(seq, payload) =>
|
||||
packetType = PacketType.Crypto
|
||||
sequenceNum = seq
|
||||
EncodePacket(payload) match {
|
||||
case packet: PlanetSideGamePacket =>
|
||||
encodePacket(packet) match {
|
||||
case Successful(payload) =>
|
||||
(
|
||||
PlanetSidePacketFlags.codec.encode(PlanetSidePacketFlags(PacketType.Normal, secured = false)).require,
|
||||
payload
|
||||
)
|
||||
case f @ Failure(_) => return f
|
||||
}
|
||||
case packet: PlanetSideControlPacket =>
|
||||
encodePacket(packet) match {
|
||||
case Successful(payload) =>
|
||||
(
|
||||
// control packets don't have flags
|
||||
BitVector.empty,
|
||||
payload
|
||||
)
|
||||
case f @ Failure(_) => return f
|
||||
}
|
||||
case packet: PlanetSideCryptoPacket =>
|
||||
encodePacket(packet) match {
|
||||
case Successful(payload) =>
|
||||
(
|
||||
PlanetSidePacketFlags.codec.encode(PlanetSidePacketFlags(PacketType.Crypto, secured = false)).require,
|
||||
payload
|
||||
)
|
||||
case f @ Failure(_) => return f
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
|
||||
case EncryptedPacket(seq, payload) =>
|
||||
secured = true
|
||||
packetType = PacketType.Normal
|
||||
sequenceNum = seq
|
||||
//encrypted packets need to be aligned to 4 bytes before encryption/decryption
|
||||
//first byte are flags, second is the sequence, and third is the pad
|
||||
paddingEncoded = hex"00".bits
|
||||
payloadEncoded = payload.bits
|
||||
}
|
||||
|
||||
//crypto packets DON'T have flags
|
||||
if (!controlPacket) {
|
||||
val flags = PlanetSidePacketFlags(packetType, secured = secured)
|
||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
||||
uint16L.encode(sequenceNum) match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err(s"Failed to marshal sequence in packet $packet: " + e.messageWithContext))
|
||||
case Successful(p) => seqEncoded = p
|
||||
}
|
||||
}
|
||||
Attempt.successful(flagsEncoded ++ seqEncoded ++ paddingEncoded ++ payloadEncoded)
|
||||
Successful(flags ++ seq ++ payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
def EncodePacket(packet: PlanetSideControlPacket): Attempt[BitVector] = {
|
||||
val opcode = packet.opcode
|
||||
var opcodeEncoded = BitVector.empty
|
||||
ControlPacketOpcode.codec.encode(opcode) match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err(s"Failed to marshal opcode in control packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => opcodeEncoded = p
|
||||
}
|
||||
var payloadEncoded = BitVector.empty
|
||||
encodePacket(packet) match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext))
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
Attempt.Successful(hex"00".bits ++ opcodeEncoded ++ payloadEncoded)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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(_) => s
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
def EncodePacket(packet: PlanetSideGamePacket): Attempt[BitVector] = {
|
||||
val opcode = packet.opcode
|
||||
var opcodeEncoded = BitVector.empty
|
||||
GamePacketOpcode.codec.encode(opcode) match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err(s"Failed to marshal opcode in game packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => opcodeEncoded = p
|
||||
}
|
||||
var payloadEncoded = BitVector.empty
|
||||
encodePacket(packet) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext))
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
Attempt.Successful(opcodeEncoded ++ payloadEncoded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the packet's own `encode` function.
|
||||
* Lowest encode call before the packet-specific implementations.
|
||||
* Transform a `PlanetSidePacket` into its `BitVector` representation.
|
||||
* @param packet the packet to encode
|
||||
* @return a `BitVector` translated from the packet's data
|
||||
*/
|
||||
private def encodePacket(packet: PlanetSidePacket): Attempt[BitVector] = packet.encode
|
||||
|
||||
/* Unmarshalling and Decoding. */
|
||||
|
||||
/**
|
||||
* A lower bound on the packet size
|
||||
*/
|
||||
final val PLANETSIDE_MIN_PACKET_SIZE = 1
|
||||
def encodePacket(packet: PlanetSidePacket): Attempt[BitVector] = {
|
||||
packet.encode match {
|
||||
case Successful(payload) =>
|
||||
packet match {
|
||||
case _: PlanetSideCryptoPacket => Successful(payload)
|
||||
case packet: PlanetSideControlPacket =>
|
||||
ControlPacketOpcode.codec.encode(packet.opcode) match {
|
||||
case Successful(opcode) => Successful(hex"00".bits ++ opcode ++ payload)
|
||||
case f @ Failure(_) => f
|
||||
}
|
||||
case packet: PlanetSideGamePacket =>
|
||||
GamePacketOpcode.codec.encode(packet.opcode) match {
|
||||
case Successful(opcode) => Successful(opcode ++ payload)
|
||||
case f @ Failure(_) => f
|
||||
}
|
||||
}
|
||||
case f @ Failure(_) => f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 crypto CryptoCoding instance for packet decryption, if this is a encrypted packet
|
||||
* @param cryptoState the current state of the connection's crypto. This is only used when decoding
|
||||
* crypto packets as they do not have opcodes
|
||||
* @return `PlanetSidePacketContainer`
|
||||
*/
|
||||
def UnmarshalPacket(msg: ByteVector, cryptoState: CryptoPacketOpcode.Type): Attempt[PlanetSidePacketContainer] = {
|
||||
if (msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||
return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
|
||||
val firstByte = msg { 0 }
|
||||
firstByte match {
|
||||
case 0x00 => unmarshalControlPacket(msg.drop(1)) //control packets dont need the first byte
|
||||
case _ => unmarshalFlaggedPacket(msg, cryptoState) //either EncryptedPacket or CryptoPacket
|
||||
def unmarshalPacket(
|
||||
msg: ByteVector,
|
||||
crypto: Option[CryptoCoding] = None,
|
||||
cryptoState: CryptoPacketOpcode.Type = CryptoPacketOpcode.Ignore
|
||||
): Attempt[(PlanetSidePacket, Option[Int])] = {
|
||||
if (msg.length < PLANETSIDE_MIN_PACKET_SIZE) {
|
||||
Failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
} else {
|
||||
msg(0) match {
|
||||
// ControlPacket
|
||||
case 0x00 => decodePacket(msg).map(p => (p, None))
|
||||
// either encrypted payload or CryptoPacket
|
||||
case _ => unmarshalFlaggedPacket(msg, cryptoState, crypto).map { case (p, s) => (p, Some(s)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `ControlPacket`.
|
||||
* It may just be encrypted (`EncryptedPacket`) or it may be involved in the encryption process itself (`CryptoPacket`).
|
||||
|
|
@ -250,106 +160,50 @@ object PacketCoding {
|
|||
*/
|
||||
private def unmarshalFlaggedPacket(
|
||||
msg: ByteVector,
|
||||
cryptoState: CryptoPacketOpcode.Type
|
||||
): Attempt[PlanetSidePacketContainer] = {
|
||||
val decodedFlags = Codec.decode[PlanetSidePacketFlags](BitVector(msg)) //get the flags
|
||||
decodedFlags match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to parse packet flags: " + e.message))
|
||||
case _ =>
|
||||
cryptoState: CryptoPacketOpcode.Type,
|
||||
crypto: Option[CryptoCoding] = None
|
||||
): Attempt[(PlanetSidePacket, Int)] = {
|
||||
val (flags, remainder) = Codec.decode[PlanetSidePacketFlags](BitVector(msg)) match {
|
||||
case Successful(DecodeResult(value, remainder)) => (value, remainder)
|
||||
case Failure(e) => return Failure(Err(s"Failed to parse packet flags: ${e.message}"))
|
||||
}
|
||||
val flags = decodedFlags.require.value
|
||||
val packetType = flags.packetType
|
||||
packetType match {
|
||||
|
||||
flags.packetType match {
|
||||
case PacketType.Normal =>
|
||||
if (!flags.secured) { //support normal packets only if they are encrypted
|
||||
return Attempt.failure(Err("Unsupported packet type: normal packets must be encryped"))
|
||||
// support normal packets only if they are encrypted
|
||||
if (!flags.secured) {
|
||||
return Failure(Err("Unsupported packet type: normal packets must be encryped"))
|
||||
}
|
||||
|
||||
case PacketType.Crypto =>
|
||||
if (flags.secured) { //support crypto packets only if they are not encrypted
|
||||
return Attempt.failure(Err("Unsupported packet type: crypto packets must be unencrypted"))
|
||||
if (flags.secured && crypto.isEmpty) {
|
||||
return Failure(Err("Unsupported packet type: crypto packets must be unencrypted"))
|
||||
}
|
||||
|
||||
case _ =>
|
||||
return Attempt.failure(Err("Unsupported packet type: " + flags.packetType.toString))
|
||||
return Failure(Err(s"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 {
|
||||
|
||||
// all packets have a two byte sequence ID
|
||||
val (sequence, payload) = uint16L.decode(remainder) match {
|
||||
case Successful(DecodeResult(value, remainder)) =>
|
||||
(value, remainder.toByteVector)
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to parse packet sequence number: " + e.message))
|
||||
case _ =>
|
||||
return Failure(Err(s"Failed to parse packet sequence number: ${e.message}"))
|
||||
}
|
||||
val sequence = decodedSeq.require.value
|
||||
val payload = decodedSeq.require.remainder.toByteVector
|
||||
packetType match {
|
||||
case PacketType.Crypto =>
|
||||
unmarshalCryptoPacket(cryptoState, sequence, payload)
|
||||
case PacketType.Normal =>
|
||||
unmarshalEncryptedPacket(sequence, payload.drop(1)) //payload is 4-byte aligned
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Successful(p) =>
|
||||
Attempt.successful(CreateControlPacket(p))
|
||||
(flags.packetType, crypto) match {
|
||||
case (PacketType.Crypto, _) =>
|
||||
CryptoPacketOpcode
|
||||
.getPacketDecoder(cryptoState)(payload.bits)
|
||||
.map(p => (p.value.asInstanceOf[PlanetSidePacket], sequence))
|
||||
case (PacketType.Normal, Some(crypto)) if flags.secured =>
|
||||
// encrypted payload is 4-byte aligned: 1b flags, 2b sequence, 1b padding
|
||||
crypto.decrypt(payload.drop(1)).map(p => decodePacket(p)).flatten.map(p => (p, sequence))
|
||||
case (PacketType.Normal, None) if !flags.secured =>
|
||||
decodePacket(payload).map(p => (p, sequence))
|
||||
case (PacketType.Normal, None) =>
|
||||
Failure(Err(s"Cannot unmarshal encrypted packet without CryptoCoding"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Successful(p) =>
|
||||
Attempt.successful(CreateGamePacket(sequence, p))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle decoding for a `CryptoPacket`.
|
||||
* @param state the current cryptographic state
|
||||
* @param sequence na
|
||||
* @param payload the packet data
|
||||
* @return a `CryptoPacket`
|
||||
*/
|
||||
private def unmarshalCryptoPacket(
|
||||
state: CryptoPacketOpcode.Type,
|
||||
sequence: Int,
|
||||
payload: ByteVector
|
||||
): Attempt[CryptoPacket] = {
|
||||
CryptoPacketOpcode.getPacketDecoder(state)(payload.bits) match {
|
||||
case Successful(a) =>
|
||||
Attempt.successful(CryptoPacket(sequence, a.value))
|
||||
case Failure(e) =>
|
||||
Attempt.failure(e.pushContext("unmarshal_crypto_packet"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle decoding for an `EncryptedPacket`.
|
||||
* The payload is already encrypted.
|
||||
* Just repackage the data.
|
||||
* @param sequence na
|
||||
* @param payload the packet data
|
||||
* @return an `EncryptedPacket`
|
||||
*/
|
||||
private def unmarshalEncryptedPacket(sequence: Int, payload: ByteVector): Attempt[EncryptedPacket] = {
|
||||
Attempt.successful(EncryptedPacket(sequence, payload))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -361,233 +215,110 @@ object PacketCoding {
|
|||
* @return `PlanetSidePacket`
|
||||
* @see `UnMarshalPacket`
|
||||
*/
|
||||
def DecodePacket(msg: ByteVector): Attempt[PlanetSidePacket] = {
|
||||
def decodePacket(msg: ByteVector): Attempt[PlanetSidePacket] = {
|
||||
if (msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||
return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
return Failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
val firstByte = msg { 0 }
|
||||
firstByte match {
|
||||
case 0x00 => DecodeControlPacket(msg.drop(1)) //control packets dont need the first byte
|
||||
case _ => DecodeGamePacket(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `ByteVector` into a `ControlPacket`.
|
||||
* @param msg the the raw data to decode
|
||||
* @return a `PlanetSideControlPacket`
|
||||
*/
|
||||
def DecodeControlPacket(msg: ByteVector): Attempt[PlanetSideControlPacket] = {
|
||||
ControlPacketOpcode.codec.decode(msg.bits) match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err("Failed to decode control packet's opcode: " + e.message))
|
||||
|
||||
case Successful(op) =>
|
||||
ControlPacketOpcode.getPacketDecoder(op.value)(op.remainder) match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err(f"Failed to parse control packet ${op.value}: " + e.messageWithContext))
|
||||
|
||||
case Successful(p) =>
|
||||
Attempt.successful(p.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `ByteVector` into a `GamePacket`.
|
||||
* @param msg the the raw data to decode
|
||||
* @return a `PlanetSideGamePacket`
|
||||
*/
|
||||
def DecodeGamePacket(msg: ByteVector): Attempt[PlanetSideGamePacket] = {
|
||||
GamePacketOpcode.codec.decode(msg.bits) match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err("Failed to decode game packet's opcode: " + e.message))
|
||||
|
||||
case Successful(opcode) =>
|
||||
GamePacketOpcode.getPacketDecoder(opcode.value)(opcode.remainder) match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err(f"Failed to parse game packet 0x${opcode.value.id}%02x: " + e.messageWithContext))
|
||||
|
||||
case Successful(p) =>
|
||||
Attempt.successful(p.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Encrypting and Decrypting. */
|
||||
|
||||
/**
|
||||
* Transform the privileged `packet` into a `RawPacket` representation to get:
|
||||
* the sequence number,
|
||||
* and the raw `ByteVector` data.
|
||||
* @param packet the unencrypted packet
|
||||
* @return paired data based on the packet
|
||||
*/
|
||||
def getPacketDataForEncryption(packet: PlanetSidePacketContainer): Attempt[(Int, ByteVector)] = {
|
||||
makeRawPacket(packet) match {
|
||||
case Successful(rawPacket) =>
|
||||
val sequenceNumber = packet match { //the sequence is variable if this is a GamePacket
|
||||
case GamePacket(_, seq, _) => seq
|
||||
case _ => 0
|
||||
}
|
||||
Successful((sequenceNumber, rawPacket.toByteVector))
|
||||
|
||||
case f @ Failure(_) =>
|
||||
f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
def makeRawPacket(packet: PlanetSidePacketContainer): Attempt[BitVector] =
|
||||
packet match {
|
||||
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 _ =>
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => Attempt.successful(opcodeEncoded.require ++ p)
|
||||
case 0x00 =>
|
||||
// control packets don't need the first byte
|
||||
ControlPacketOpcode.codec.decode(msg.drop(1).bits) match {
|
||||
case Successful(op) =>
|
||||
ControlPacketOpcode.getPacketDecoder(op.value)(op.remainder) match {
|
||||
case Successful(p) => Successful(p.value)
|
||||
case Failure(e) => Failure(Err(e.messageWithContext))
|
||||
}
|
||||
case Failure(e) => Failure(Err(e.message))
|
||||
}
|
||||
|
||||
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 _ =>
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => Attempt.successful(hex"00".bits ++ opcodeEncoded.require ++ p)
|
||||
}
|
||||
}
|
||||
|
||||
case _ =>
|
||||
throw new IllegalArgumentException("Unsupported packet container type")
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform encryption on the packet's raw data.
|
||||
* @param crypto the current cryptographic state
|
||||
* @param sequenceNumber na
|
||||
* @param rawPacket a `ByteVector` that represents the packet data
|
||||
* @return an `EncryptedPacket`
|
||||
*/
|
||||
def encryptPacket(
|
||||
crypto: CryptoInterface.CryptoStateWithMAC,
|
||||
sequenceNumber: Int,
|
||||
rawPacket: ByteVector
|
||||
): Attempt[EncryptedPacket] = {
|
||||
val packetMac = crypto.macForEncrypt(rawPacket)
|
||||
val packetNoPadding = rawPacket ++ packetMac //opcode, payload, and MAC
|
||||
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE
|
||||
val paddingNeeded = CryptoInterface.RC5_BLOCK_SIZE - remainder - 1 //minus 1 because of a mandatory padding bit
|
||||
val paddingEncoded = uint8L.encode(paddingNeeded.toInt).require
|
||||
val packetWithPadding = packetNoPadding ++ ByteVector.fill(paddingNeeded)(0x00) ++ paddingEncoded.toByteVector
|
||||
val encryptedPayload =
|
||||
crypto.encrypt(packetWithPadding) //raw packets plus MAC, padded to the nearest 16 byte boundary
|
||||
Attempt.successful(EncryptedPacket(sequenceNumber, encryptedPayload))
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform decryption on an `EncryptedPacket`.
|
||||
* @param crypto the current state of the connection's crypto
|
||||
* @param packet an encrypted packet
|
||||
* @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
|
||||
GamePacketOpcode.codec.decode(msg.bits) match {
|
||||
case Successful(opcode) =>
|
||||
GamePacketOpcode.getPacketDecoder(opcode.value)(opcode.remainder) match {
|
||||
case Failure(e) =>
|
||||
Failure(Err(f"Failed to parse game packet 0x${opcode.value.id}%02x: " + e.messageWithContext))
|
||||
case Successful(p) => Successful(p.value)
|
||||
}
|
||||
case Failure(e) => Failure(Err("Failed to decode game packet's opcode: " + e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
case class CryptoCoding(
|
||||
rc5EncryptionKey: Key,
|
||||
rc5DecryptionKey: Key,
|
||||
macEncryptionKey: ByteVector,
|
||||
macDecryptionKey: ByteVector
|
||||
) {
|
||||
private val iv = BigInt(64, random)
|
||||
private val rc5Spec = new RC5ParameterSpec(0, 16, 32)
|
||||
private val rc5Encrypt = Cipher.getInstance("RC5/ECB/NoPadding")
|
||||
private val rc5Decrypt = Cipher.getInstance("RC5/ECB/NoPadding")
|
||||
rc5Encrypt.init(Cipher.ENCRYPT_MODE, rc5EncryptionKey, rc5Spec)
|
||||
rc5Decrypt.init(Cipher.DECRYPT_MODE, rc5DecryptionKey, rc5Spec)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
padding match {
|
||||
case Failure(e) => return Attempt.failure(Err("Failed to decode the encrypted padding length: " + e.message))
|
||||
case _ =>
|
||||
def encrypt(packet: PlanetSidePacket): Attempt[ByteVector] = {
|
||||
encodePacket(packet) match {
|
||||
case Successful(data) =>
|
||||
encrypt(data.toByteVector)
|
||||
case f @ Failure(_) =>
|
||||
f
|
||||
}
|
||||
}
|
||||
|
||||
val macSize = CryptoInterface.MD5_MAC_SIZE
|
||||
val macDecoder = bytes(macSize)
|
||||
val payloadNoPadding = payloadDecrypted.dropRight(1 + padding.require.value)
|
||||
val payloadMac = payloadNoPadding.takeRight(macSize)
|
||||
/*
|
||||
println("Payload: " + packet.payload)
|
||||
println("DecPayload: " + payloadDecrypted)
|
||||
println("DecPayloadNoLen: " + payloadJustLen)
|
||||
println("Padding: " + padding.require.value)
|
||||
println("NoPadding: " + payloadNoPadding)
|
||||
println("Mac: " + payloadMac)
|
||||
println("NoMac: " + payloadNoMac)
|
||||
*/
|
||||
val mac = macDecoder.decode(payloadMac.bits)
|
||||
mac match {
|
||||
case Failure(e) => return Attempt.failure(Err("Failed to extract the encrypted MAC: " + e.message))
|
||||
case _ =>
|
||||
def encrypt(
|
||||
data: ByteVector
|
||||
): Attempt[ByteVector] = {
|
||||
// This is basically X9.23 padding, except that the length byte is -1 because it doesn't count itself
|
||||
val packetNoPadding = data ++ new Md5Mac(macEncryptionKey).updateFinal(data) // opcode, payload, and MAC
|
||||
val remainder = packetNoPadding.length % RC5_BLOCK_SIZE
|
||||
val paddingNeeded = RC5_BLOCK_SIZE - remainder - 1 // minus 1 because of a mandatory padding byte
|
||||
val paddingEncoded = uint8L.encode(paddingNeeded.toInt).require
|
||||
val packetWithPadding = packetNoPadding ++ ByteVector.fill(paddingNeeded)(0x00) ++ paddingEncoded.toByteVector
|
||||
// raw packets plus MAC, padded to the nearest 8 byte boundary
|
||||
try {
|
||||
Successful(ByteVector.view(rc5Encrypt.doFinal(packetWithPadding.toArray)))
|
||||
} catch {
|
||||
case e: Throwable => Failure(Err(s"encrypt error: '${e.getMessage}' data: ${packetWithPadding.toHex}"))
|
||||
}
|
||||
}
|
||||
|
||||
val payloadNoMac = payloadNoPadding.dropRight(macSize)
|
||||
val computedMac = crypto.macForDecrypt(payloadNoMac)
|
||||
if (!CryptoInterface.verifyMAC(computedMac, mac.require.value)) { //verify that the MAC matches
|
||||
throw new SecurityException("Invalid packet MAC")
|
||||
def decrypt(data: ByteVector): Attempt[ByteVector] = {
|
||||
val payloadDecrypted =
|
||||
try {
|
||||
ByteVector.view(rc5Decrypt.doFinal(data.toArray))
|
||||
} catch {
|
||||
case e: Throwable => return Failure(Err(e.getMessage))
|
||||
}
|
||||
|
||||
// last byte is the padding length
|
||||
val padding = uint8L.decode(payloadDecrypted.takeRight(1).bits) match {
|
||||
case Successful(padding) => padding.value
|
||||
case Failure(e) => return Failure(Err(s"Failed to decode the encrypted padding length: ${e.message}"))
|
||||
}
|
||||
|
||||
val payloadNoPadding = payloadDecrypted.dropRight(1 + padding)
|
||||
val payloadMac = payloadNoPadding.takeRight(Md5Mac.MACLENGTH)
|
||||
|
||||
val mac = bytes(Md5Mac.MACLENGTH).decode(payloadMac.bits) match {
|
||||
case Failure(e) => return Failure(Err("Failed to extract the encrypted MAC: " + e.message))
|
||||
case Successful(mac) => mac.value
|
||||
}
|
||||
|
||||
val payloadNoMac = payloadNoPadding.dropRight(Md5Mac.MACLENGTH)
|
||||
val computedMac = new Md5Mac(macDecryptionKey).updateFinal(payloadNoMac)
|
||||
|
||||
if (!Md5Mac.verifyMac(computedMac, mac)) {
|
||||
return Failure(Err("Invalid packet MAC"))
|
||||
}
|
||||
|
||||
if (payloadNoMac.length < PLANETSIDE_MIN_PACKET_SIZE) {
|
||||
return Failure(
|
||||
Err(s"Decrypted packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")
|
||||
)
|
||||
}
|
||||
|
||||
Successful(payloadNoMac)
|
||||
}
|
||||
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")
|
||||
)
|
||||
}
|
||||
Successful(payloadNoMac)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.control
|
||||
|
||||
import net.psforever.packet.ControlPacketOpcode.Type
|
||||
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PlanetSideControlPacket}
|
||||
import scodec.Codec
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.codecs._
|
||||
|
||||
final case class HandleGamePacket(len: Int, stream: ByteVector, rest: BitVector = BitVector.empty)
|
||||
extends PlanetSideControlPacket {
|
||||
def opcode = ControlPacketOpcode.HandleGamePacket
|
||||
def encode = HandleGamePacket.encode(this)
|
||||
def opcode: Type = ControlPacketOpcode.HandleGamePacket
|
||||
def encode: Attempt[BitVector] = HandleGamePacket.encode(this)
|
||||
}
|
||||
|
||||
object HandleGamePacket extends Marshallable[HandleGamePacket] {
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.control
|
||||
|
||||
import net.psforever.packet.PlanetSidePacket
|
||||
|
||||
/**
|
||||
* Message for holding a series of packets being moved through the system (server),
|
||||
* eventually be bundled into a `MultiPacketEx` and dispatched to the client.
|
||||
* Invalid packets are eliminated at the time of creation.
|
||||
* At least one packet is necessary.
|
||||
* @param packets a series of packets to be bundled together;
|
||||
* this list is effectively immutable;
|
||||
* the only way to access these packets is through pattern matching
|
||||
*/
|
||||
final case class MultiPacketBundle(private var packets: List[PlanetSidePacket]) {
|
||||
MultiPacketBundle.collectValidPackets(packets) match {
|
||||
case Nil =>
|
||||
throw new IllegalArgumentException("can not create with zero packets")
|
||||
case list =>
|
||||
packets = list
|
||||
}
|
||||
|
||||
def +(t: MultiPacketBundle): MultiPacketBundle =
|
||||
t match {
|
||||
case MultiPacketBundle(list) =>
|
||||
MultiPacketBundle(packets ++ list)
|
||||
case _ =>
|
||||
MultiPacketBundle(packets)
|
||||
}
|
||||
}
|
||||
|
||||
object MultiPacketBundle {
|
||||
|
||||
/**
|
||||
* Accept a series of packets of a specific supertype (`PlanetSidePacket`)
|
||||
* and filter out subtypes that should be excluded.
|
||||
* Show a generic disclaimer if any packets were filtered.
|
||||
* Two of the four subclasses of `PlanetSidePacket` are accepted - `PlanetSideGamePacket` and `PlanetSideControlPacket`.
|
||||
* @param packets a series of packets
|
||||
* @return the accepted packets from the original group
|
||||
*/
|
||||
def collectValidPackets(packets: List[PlanetSidePacket]): List[PlanetSidePacket] = {
|
||||
import net.psforever.packet.{PlanetSideGamePacket, PlanetSideControlPacket}
|
||||
val (good, bad) = packets.partition({
|
||||
case _: PlanetSideGamePacket => true
|
||||
case _: PlanetSideControlPacket => true
|
||||
case _ => false
|
||||
})
|
||||
if (bad.nonEmpty) {
|
||||
org.log4s
|
||||
.getLogger("MultiPacketBundle")
|
||||
.warn(s"attempted to include packet types that are not in the whitelist; ${bad.size} items have been excluded")
|
||||
}
|
||||
good
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulator for packets that will eventually be bundled and submitted for composing a `MultiPacketEx` packet.
|
||||
*/
|
||||
class MultiPacketCollector() {
|
||||
private var bundle: List[PlanetSidePacket] = List.empty
|
||||
|
||||
def Add(t: PlanetSidePacket): Unit = Add(List(t))
|
||||
|
||||
def Add(t: MultiPacketBundle): Unit =
|
||||
t match {
|
||||
case MultiPacketBundle(list) =>
|
||||
Add(list)
|
||||
}
|
||||
|
||||
def Add(t: List[PlanetSidePacket]): Unit = {
|
||||
if (t.nonEmpty) {
|
||||
bundle = bundle ++ t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the internal collection of packets.
|
||||
* Reset the internal list of packets by clearing it.
|
||||
* @return a loaded `MultiPacketBundle` object, or `None`
|
||||
*/
|
||||
def Bundle: Option[MultiPacketBundle] = {
|
||||
bundle match {
|
||||
case Nil =>
|
||||
None
|
||||
case list =>
|
||||
val out = MultiPacketBundle(list)
|
||||
bundle = List.empty
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MultiPacketCollector {
|
||||
|
||||
/**
|
||||
* Overload constructor that accepts initial packets.
|
||||
* @param bundle previously bundled packets
|
||||
* @return a `MultiPacketCollector` object
|
||||
*/
|
||||
def apply(bundle: MultiPacketBundle): MultiPacketCollector = {
|
||||
val obj = new MultiPacketCollector()
|
||||
obj.Add(bundle)
|
||||
obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload constructor that accepts initial packets.
|
||||
* @param packets a series of packets
|
||||
* @return a `MultiPacketCollector` object
|
||||
*/
|
||||
def apply(packets: List[PlanetSidePacket]): MultiPacketCollector = {
|
||||
val obj = new MultiPacketCollector()
|
||||
obj.Add(packets)
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.crypto
|
||||
|
||||
import net.psforever.packet.CryptoPacketOpcode.Type
|
||||
import net.psforever.packet.{CryptoPacketOpcode, Marshallable, PlanetSideCryptoPacket}
|
||||
import scodec.Codec
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.bits.{ByteVector, _}
|
||||
import scodec.codecs._
|
||||
|
||||
final case class ClientChallengeXchg(time: Long, challenge: ByteVector, p: ByteVector, g: ByteVector)
|
||||
extends PlanetSideCryptoPacket {
|
||||
def opcode = CryptoPacketOpcode.ClientChallengeXchg
|
||||
def encode = ClientChallengeXchg.encode(this)
|
||||
def opcode: Type = CryptoPacketOpcode.ClientChallengeXchg
|
||||
def encode: Attempt[BitVector] = ClientChallengeXchg.encode(this)
|
||||
}
|
||||
|
||||
object ClientChallengeXchg extends Marshallable[ClientChallengeXchg] {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ object PlanetSideZoneID {
|
|||
/**
|
||||
* Is sent by the PlanetSide world server when sending character selection screen state. Provides metadata
|
||||
* about a certain character for rendering purposes (zone background, etc). Acts as an array insert for the
|
||||
* client character list. A blank displayed character is most likely caused by a mismatch between an
|
||||
* ObjectCreateMessage GUID and the GUID from this message.
|
||||
* client character list. A blank displayed character is most likely caused by either:
|
||||
* - a mismatch between an ObjectCreateMessage GUID and the GUID from this message.
|
||||
* - bundling CharacterInfoMessage with OCDM, they should appear one after another
|
||||
*
|
||||
* @param finished True when there are no more characters to give info on
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ class LocalService(zone: Zone) extends Actor {
|
|||
|
||||
//response from HackClearActor
|
||||
case HackClearActor.ClearTheHack(target_guid, _, unk1, unk2) =>
|
||||
log.warn(s"Clearing hack for $target_guid")
|
||||
log.info(s"Clearing hack for $target_guid")
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(
|
||||
s"/${zone.id}/Local",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import net.psforever.types.ChatMessageType
|
|||
import pureconfig.ConfigConvert.viaNonEmptyStringOpt
|
||||
import pureconfig.ConfigReader.Result
|
||||
import pureconfig.{ConfigConvert, ConfigSource}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.reflect.ClassTag
|
||||
import pureconfig.generic.auto._ // intellij: this is not unused
|
||||
|
|
@ -116,8 +115,8 @@ case class NetworkConfig(
|
|||
)
|
||||
|
||||
case class SessionConfig(
|
||||
inboundGraceTime: Duration,
|
||||
outboundGraceTime: Duration
|
||||
inboundGraceTime: FiniteDuration,
|
||||
outboundGraceTime: FiniteDuration
|
||||
)
|
||||
|
||||
case class GameConfig(
|
||||
|
|
|
|||
|
|
@ -235,9 +235,7 @@ object DefinitionUtil {
|
|||
player.Slot(33).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm_AP)
|
||||
player.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(faction))
|
||||
player.Slot(39).Equipment = SimpleItem(GlobalDefinitions.remote_electronics_kit)
|
||||
player.Inventory.Items.foreach {
|
||||
_.obj.Faction = faction
|
||||
}
|
||||
player.Inventory.Items.foreach(_.obj.Faction = faction)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -266,9 +264,6 @@ object DefinitionUtil {
|
|||
// But macros cannot be called from the project they're defined in, and moving this to another project is not easy
|
||||
// Making GlobalDefinitions iterable (map etc) should be the preferred solution
|
||||
def fromString(name: String): BasicDefinition = {
|
||||
//println(
|
||||
// s"${universe.typeOf[GlobalDefinitions.type].member(universe.TermName(name))} ${universe.typeOf[GlobalDefinitions.type].decl(universe.TermName(name))}"
|
||||
//)
|
||||
universe.typeOf[GlobalDefinitions.type].decl(universe.TermName(name))
|
||||
val method = universe.typeOf[GlobalDefinitions.type].member(universe.TermName(name)).asMethod
|
||||
instanceMirror.reflectMethod(method).apply().asInstanceOf[BasicDefinition]
|
||||
|
|
|
|||
31
src/main/scala/net/psforever/util/DiffieHellman.scala
Normal file
31
src/main/scala/net/psforever/util/DiffieHellman.scala
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package net.psforever.util
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
/** Simple DH implementation
|
||||
* We can not use Java's built-in DH because it requires much larger p values than the ones that are used
|
||||
* for key exchange by the client (which are 128 bits).
|
||||
*/
|
||||
case class DiffieHellman(p: Array[Byte], g: Array[Byte]) {
|
||||
import DiffieHellman._
|
||||
|
||||
private val _p = BigInt(1, p)
|
||||
private val _g = BigInt(1, g)
|
||||
private val privateKey: BigInt = BigInt(128, random)
|
||||
|
||||
val publicKey: Array[Byte] = bytes(_g.modPow(privateKey, _p))
|
||||
|
||||
/** Agree on shared key */
|
||||
def agree(otherKey: Array[Byte]): Array[Byte] = {
|
||||
bytes(BigInt(1, otherKey).modPow(privateKey, _p))
|
||||
}
|
||||
|
||||
/** Return BigInt as 16 byte array (same size as private key) */
|
||||
private def bytes(b: BigInt): Array[Byte] = {
|
||||
b.toByteArray.takeRight(16).reverse.padTo(16, 0x0.toByte).reverse
|
||||
}
|
||||
}
|
||||
|
||||
object DiffieHellman {
|
||||
private val random = new SecureRandom()
|
||||
}
|
||||
261
src/main/scala/net/psforever/util/Md5Mac.scala
Normal file
261
src/main/scala/net/psforever/util/Md5Mac.scala
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
package net.psforever.util
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
object Md5Mac {
|
||||
val BLOCKSIZE = 64
|
||||
val DIGESTSIZE = 16
|
||||
val MACLENGTH = 16
|
||||
val KEYLENGTH = 16
|
||||
|
||||
/** Checks if two Message Authentication Codes are the same in constant time,
|
||||
* preventing a timing attack for MAC forgery
|
||||
* @param mac1 A MAC value
|
||||
* @param mac2 Another MAC value
|
||||
*/
|
||||
def verifyMac(mac1: ByteVector, mac2: ByteVector): Boolean = {
|
||||
var okay = true
|
||||
|
||||
// prevent byte by byte guessing
|
||||
if (mac1.length != mac2.length)
|
||||
return false
|
||||
|
||||
for (i <- 0 until mac1.length.toInt) {
|
||||
okay = okay && mac1 { i } == mac2 { i }
|
||||
}
|
||||
|
||||
okay
|
||||
}
|
||||
}
|
||||
|
||||
/** MD5-MAC is a ancient MAC algorithm from the 90s that nobody uses anymore. Not to be confused with HMAC-MD5.
|
||||
* A description of the algorithm can be found at http://cacr.uwaterloo.ca/hac/about/chap9.pdf, 9.69 Algorithm MD5-MAC
|
||||
* There appear to be two implementations: In older versions of CryptoPP (2007) and OpenCL (2001) (nowadays called
|
||||
* Botan and not to be confused with the OpenCL standard from Khronos).
|
||||
* Both libraries have since removed this code. This file is a Scala port of the OpenCL implementation.
|
||||
* Source: https://github.com/sghiassy/Code-Reading-Book/blob/master/OpenCL/src/md5mac.cpp
|
||||
*/
|
||||
class Md5Mac(val key: ByteVector) {
|
||||
import Md5Mac._
|
||||
|
||||
private val buffer: ListBuffer[Byte] = ListBuffer.fill(BLOCKSIZE)(0)
|
||||
private val digest: ListBuffer[Byte] = ListBuffer.fill(DIGESTSIZE)(0)
|
||||
private val m: ListBuffer[Byte] = ListBuffer.fill(32)(0)
|
||||
private val k1: ListBuffer[Byte] = ListBuffer.fill(16)(0)
|
||||
private val k2: ListBuffer[Byte] = ListBuffer.fill(16)(0)
|
||||
private val k3: ListBuffer[Byte] = ListBuffer.fill(BLOCKSIZE)(0)
|
||||
private var count: Long = 0
|
||||
private var position: Int = 0
|
||||
|
||||
private val t: Seq[Seq[Byte]] = Seq(
|
||||
Seq(0x97, 0xef, 0x45, 0xac, 0x29, 0x0f, 0x43, 0xcd, 0x45, 0x7e, 0x1b, 0x55, 0x1c, 0x80, 0x11, 0x34),
|
||||
Seq(0xb1, 0x77, 0xce, 0x96, 0x2e, 0x72, 0x8e, 0x7c, 0x5f, 0x5a, 0xab, 0x0a, 0x36, 0x43, 0xbe, 0x18),
|
||||
Seq(0x9d, 0x21, 0xb4, 0x21, 0xbc, 0x87, 0xb9, 0x4d, 0xa2, 0x9d, 0x27, 0xbd, 0xc7, 0x5b, 0xd7, 0xc3)
|
||||
).map(_.map(_.toByte))
|
||||
|
||||
assert(key.length == KEYLENGTH, s"key length must be ${KEYLENGTH}, not ${key.length}")
|
||||
doKey()
|
||||
|
||||
private def doKey() = {
|
||||
val ek: ListBuffer[Byte] = ListBuffer.fill(48)(0)
|
||||
val data: ListBuffer[Byte] = ListBuffer.fill(128)(0)
|
||||
(0 until 16).foreach(j => {
|
||||
data(j) = key(j % key.length)
|
||||
data(j + 112) = key(j % key.length)
|
||||
})
|
||||
|
||||
(0 until 3).foreach(j => {
|
||||
digest.patchInPlace(0, ByteVector.fromInt(0x67452301).toArray, 4)
|
||||
digest.patchInPlace(4, ByteVector.fromInt(0xefcdab89).toArray, 4)
|
||||
digest.patchInPlace(8, ByteVector.fromInt(0x98badcfe).toArray, 4)
|
||||
digest.patchInPlace(12, ByteVector.fromInt(0x10325476).toArray, 4)
|
||||
|
||||
(16 until 112).foreach(k => data(k) = t((j + (k - 16) / 16) % 3)(k % 16))
|
||||
|
||||
hash(data.toSeq)
|
||||
hash(data.drop(64).toSeq)
|
||||
|
||||
ek.patchInPlace(4 * 4 * j, digest.slice(0, 4), 4)
|
||||
ek.patchInPlace((4 * 4 * j) + 4, digest.slice(4, 8), 4)
|
||||
ek.patchInPlace((4 * 4 * j) + 8, digest.slice(8, 12), 4)
|
||||
ek.patchInPlace((4 * 4 * j) + 12, digest.slice(12, 16), 4)
|
||||
})
|
||||
|
||||
k1.patchInPlace(0, ek.take(16), 16)
|
||||
digest.patchInPlace(0, ek.take(16), 16)
|
||||
k2.patchInPlace(0, ek.slice(16, 32), 16)
|
||||
|
||||
(0 until 16).foreach(j => k3(j) = ek(((8 + j / 4) * 4) + (3 - j % 4)))
|
||||
(16 until 64).foreach(j => k3(j) = (k3(j % 16) ^ t((j - 16) / 16)(j % 16)).toByte)
|
||||
}
|
||||
|
||||
private def hash(input: Seq[Byte]) = {
|
||||
(0 until 16).foreach(j => {
|
||||
m.patchInPlace(j * 4, Array[Byte](input(4 * j + 3), input(4 * j + 2), input(4 * j + 1), input(4 * j + 0)), 4)
|
||||
})
|
||||
|
||||
var a = mkInt(digest.drop(0))
|
||||
var c = mkInt(digest.drop(2 * 4))
|
||||
var b = mkInt(digest.drop(1 * 4))
|
||||
var d = mkInt(digest.drop(3 * 4))
|
||||
|
||||
a = ff(a, b, c, d, mkInt(m, 0 * 4), 7, 0xd76aa478)
|
||||
d = ff(d, a, b, c, mkInt(m, 1 * 4), 12, 0xe8c7b756)
|
||||
c = ff(c, d, a, b, mkInt(m, 2 * 4), 17, 0x242070db)
|
||||
b = ff(b, c, d, a, mkInt(m, 3 * 4), 22, 0xc1bdceee)
|
||||
a = ff(a, b, c, d, mkInt(m, 4 * 4), 7, 0xf57c0faf)
|
||||
d = ff(d, a, b, c, mkInt(m, 5 * 4), 12, 0x4787c62a)
|
||||
c = ff(c, d, a, b, mkInt(m, 6 * 4), 17, 0xa8304613)
|
||||
b = ff(b, c, d, a, mkInt(m, 7 * 4), 22, 0xfd469501)
|
||||
a = ff(a, b, c, d, mkInt(m, 8 * 4), 7, 0x698098d8)
|
||||
d = ff(d, a, b, c, mkInt(m, 9 * 4), 12, 0x8b44f7af)
|
||||
c = ff(c, d, a, b, mkInt(m, 10 * 4), 17, 0xffff5bb1)
|
||||
b = ff(b, c, d, a, mkInt(m, 11 * 4), 22, 0x895cd7be)
|
||||
a = ff(a, b, c, d, mkInt(m, 12 * 4), 7, 0x6b901122)
|
||||
d = ff(d, a, b, c, mkInt(m, 13 * 4), 12, 0xfd987193)
|
||||
c = ff(c, d, a, b, mkInt(m, 14 * 4), 17, 0xa679438e)
|
||||
b = ff(b, c, d, a, mkInt(m, 15 * 4), 22, 0x49b40821)
|
||||
|
||||
a = gg(a, b, c, d, mkInt(m, 1 * 4), 5, 0xf61e2562)
|
||||
d = gg(d, a, b, c, mkInt(m, 6 * 4), 9, 0xc040b340)
|
||||
c = gg(c, d, a, b, mkInt(m, 11 * 4), 14, 0x265e5a51)
|
||||
b = gg(b, c, d, a, mkInt(m, 0 * 4), 20, 0xe9b6c7aa)
|
||||
a = gg(a, b, c, d, mkInt(m, 5 * 4), 5, 0xd62f105d)
|
||||
d = gg(d, a, b, c, mkInt(m, 10 * 4), 9, 0x02441453)
|
||||
c = gg(c, d, a, b, mkInt(m, 15 * 4), 14, 0xd8a1e681)
|
||||
b = gg(b, c, d, a, mkInt(m, 4 * 4), 20, 0xe7d3fbc8)
|
||||
a = gg(a, b, c, d, mkInt(m, 9 * 4), 5, 0x21e1cde6)
|
||||
d = gg(d, a, b, c, mkInt(m, 14 * 4), 9, 0xc33707d6)
|
||||
c = gg(c, d, a, b, mkInt(m, 3 * 4), 14, 0xf4d50d87)
|
||||
b = gg(b, c, d, a, mkInt(m, 8 * 4), 20, 0x455a14ed)
|
||||
a = gg(a, b, c, d, mkInt(m, 13 * 4), 5, 0xa9e3e905)
|
||||
d = gg(d, a, b, c, mkInt(m, 2 * 4), 9, 0xfcefa3f8)
|
||||
c = gg(c, d, a, b, mkInt(m, 7 * 4), 14, 0x676f02d9)
|
||||
b = gg(b, c, d, a, mkInt(m, 12 * 4), 20, 0x8d2a4c8a)
|
||||
|
||||
a = hh(a, b, c, d, mkInt(m, 5 * 4), 4, 0xfffa3942)
|
||||
d = hh(d, a, b, c, mkInt(m, 8 * 4), 11, 0x8771f681)
|
||||
c = hh(c, d, a, b, mkInt(m, 11 * 4), 16, 0x6d9d6122)
|
||||
b = hh(b, c, d, a, mkInt(m, 14 * 4), 23, 0xfde5380c)
|
||||
a = hh(a, b, c, d, mkInt(m, 1 * 4), 4, 0xa4beea44)
|
||||
d = hh(d, a, b, c, mkInt(m, 4 * 4), 11, 0x4bdecfa9)
|
||||
c = hh(c, d, a, b, mkInt(m, 7 * 4), 16, 0xf6bb4b60)
|
||||
b = hh(b, c, d, a, mkInt(m, 10 * 4), 23, 0xbebfbc70)
|
||||
a = hh(a, b, c, d, mkInt(m, 13 * 4), 4, 0x289b7ec6)
|
||||
d = hh(d, a, b, c, mkInt(m, 0 * 4), 11, 0xeaa127fa)
|
||||
c = hh(c, d, a, b, mkInt(m, 3 * 4), 16, 0xd4ef3085)
|
||||
b = hh(b, c, d, a, mkInt(m, 6 * 4), 23, 0x04881d05)
|
||||
a = hh(a, b, c, d, mkInt(m, 9 * 4), 4, 0xd9d4d039)
|
||||
d = hh(d, a, b, c, mkInt(m, 12 * 4), 11, 0xe6db99e5)
|
||||
c = hh(c, d, a, b, mkInt(m, 15 * 4), 16, 0x1fa27cf8)
|
||||
b = hh(b, c, d, a, mkInt(m, 2 * 4), 23, 0xc4ac5665)
|
||||
|
||||
a = ii(a, b, c, d, mkInt(m, 0 * 4), 6, 0xf4292244)
|
||||
d = ii(d, a, b, c, mkInt(m, 7 * 4), 10, 0x432aff97)
|
||||
c = ii(c, d, a, b, mkInt(m, 14 * 4), 15, 0xab9423a7)
|
||||
b = ii(b, c, d, a, mkInt(m, 5 * 4), 21, 0xfc93a039)
|
||||
a = ii(a, b, c, d, mkInt(m, 12 * 4), 6, 0x655b59c3)
|
||||
d = ii(d, a, b, c, mkInt(m, 3 * 4), 10, 0x8f0ccc92)
|
||||
c = ii(c, d, a, b, mkInt(m, 10 * 4), 15, 0xffeff47d)
|
||||
b = ii(b, c, d, a, mkInt(m, 1 * 4), 21, 0x85845dd1)
|
||||
a = ii(a, b, c, d, mkInt(m, 8 * 4), 6, 0x6fa87e4f)
|
||||
d = ii(d, a, b, c, mkInt(m, 15 * 4), 10, 0xfe2ce6e0)
|
||||
c = ii(c, d, a, b, mkInt(m, 6 * 4), 15, 0xa3014314)
|
||||
b = ii(b, c, d, a, mkInt(m, 13 * 4), 21, 0x4e0811a1)
|
||||
a = ii(a, b, c, d, mkInt(m, 4 * 4), 6, 0xf7537e82)
|
||||
d = ii(d, a, b, c, mkInt(m, 11 * 4), 10, 0xbd3af235)
|
||||
c = ii(c, d, a, b, mkInt(m, 2 * 4), 15, 0x2ad7d2bb)
|
||||
b = ii(b, c, d, a, mkInt(m, 9 * 4), 21, 0xeb86d391)
|
||||
|
||||
digest.patchInPlace(0, ByteVector.fromInt(mkInt(digest, 0) + a).toArray, 4)
|
||||
digest.patchInPlace(4, ByteVector.fromInt(mkInt(digest, 4) + b).toArray, 4)
|
||||
digest.patchInPlace(8, ByteVector.fromInt(mkInt(digest, 8) + c).toArray, 4)
|
||||
digest.patchInPlace(12, ByteVector.fromInt(mkInt(digest, 12) + d).toArray, 4)
|
||||
}
|
||||
|
||||
private def mkInt(lb: Iterable[Byte], pos: Int = 0): Int = {
|
||||
ByteVector.view(lb.slice(pos, pos + 4).toArray).toInt()
|
||||
}
|
||||
|
||||
private def ff(a: Int, b: Int, c: Int, d: Int, msg: Int, shift: Int, magic: Int): Int = {
|
||||
val r = a + ((d ^ (b & (c ^ d))) + msg + magic + mkInt(k2, 0))
|
||||
Integer.rotateLeft(r, shift) + b
|
||||
}
|
||||
|
||||
private def gg(a: Int, b: Int, c: Int, d: Int, msg: Int, shift: Int, magic: Int): Int = {
|
||||
val r = a + ((c ^ ((b ^ c) & d)) + msg + magic + mkInt(k2, 4))
|
||||
Integer.rotateLeft(r, shift) + b
|
||||
}
|
||||
|
||||
private def hh(a: Int, b: Int, c: Int, d: Int, msg: Int, shift: Int, magic: Int): Int = {
|
||||
val r = a + ((b ^ c ^ d) + msg + magic + mkInt(k2, 8))
|
||||
Integer.rotateLeft(r, shift) + b
|
||||
}
|
||||
|
||||
private def ii(a: Int, b: Int, c: Int, d: Int, msg: Int, shift: Int, magic: Int): Int = {
|
||||
val r = a + ((c ^ (b | ~d)) + msg + magic + mkInt(k2, 12))
|
||||
Integer.rotateLeft(r, shift) + b
|
||||
}
|
||||
|
||||
def update(bytes: ByteVector) = {
|
||||
count += bytes.length
|
||||
var length = bytes.length
|
||||
buffer.patchInPlace(
|
||||
position,
|
||||
bytes.take(math.min(BLOCKSIZE, bytes.length)).toIterable,
|
||||
math.min(BLOCKSIZE, bytes.length).toInt
|
||||
)
|
||||
|
||||
if (position + bytes.length >= BLOCKSIZE) {
|
||||
hash(buffer.toSeq)
|
||||
var input = bytes.drop(BLOCKSIZE - position)
|
||||
length = bytes.length - (BLOCKSIZE - position)
|
||||
while (length >= BLOCKSIZE) {
|
||||
hash(input.toSeq)
|
||||
input = input.drop(BLOCKSIZE)
|
||||
length -= BLOCKSIZE
|
||||
}
|
||||
buffer.patchInPlace(0, input.toIterable, input.length.toInt)
|
||||
position = 0
|
||||
}
|
||||
position += length.toInt
|
||||
}
|
||||
|
||||
/** Perform final hash calculations and reset the state
|
||||
* @return the hash
|
||||
*/
|
||||
def doFinal(length: Int = MACLENGTH): ByteVector = {
|
||||
val output: ListBuffer[Byte] = ListBuffer.fill(MACLENGTH)(0)
|
||||
buffer(position) = 0x80.toByte
|
||||
(position + 1 until BLOCKSIZE).foreach(i => buffer(i) = 0)
|
||||
if (position >= BLOCKSIZE - 8) {
|
||||
hash(buffer.toSeq)
|
||||
buffer.mapInPlace(_ => 0)
|
||||
}
|
||||
|
||||
(BLOCKSIZE - 8 until BLOCKSIZE).foreach(i => buffer(i) = ByteVector.fromLong(8 * count)(7 - (i % 8)))
|
||||
|
||||
hash(buffer.toSeq)
|
||||
hash(k3.toSeq)
|
||||
|
||||
(0 until MACLENGTH).foreach(i => output(i) = digest((i / 4) * 4 + (3 - (i % 4))))
|
||||
|
||||
count = 0
|
||||
position = 0
|
||||
digest.patchInPlace(0, k1, digest.length)
|
||||
|
||||
if (length == MACLENGTH) {
|
||||
ByteVector.view(output.toArray)
|
||||
} else {
|
||||
ByteVector.view((0 until length).map(i => output(i % Md5Mac.DIGESTSIZE)).toArray)
|
||||
}
|
||||
}
|
||||
|
||||
/** Shorthand for `update` and `doFinal` */
|
||||
def updateFinal(bytes: ByteVector, length: Int = MACLENGTH): ByteVector = {
|
||||
update(bytes)
|
||||
doFinal(length)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -319,7 +319,7 @@ object Zones {
|
|||
.addLocalObject(obj.guid, Locker.Constructor(obj.position), owningBuildingGuid = ownerGuid)
|
||||
|
||||
case "lock_external" | "lock_garage" | "lock_small" =>
|
||||
val closestDoor = doors.minBy(d => Vector3.DistanceSquared(d.position, obj.position))
|
||||
val closestDoor = doors.minBy(d => Vector3.Distance(d.position, obj.position))
|
||||
|
||||
// Since tech plant garage locks are the only type where the lock does not face the same direction as the door we need to apply an offset for those, otherwise the door won't operate properly when checking inside/outside angles.
|
||||
val yawOffset = if (obj.objectType == "lock_garage") 90 else 0
|
||||
|
|
@ -481,7 +481,7 @@ object Zones {
|
|||
),
|
||||
owningBuildingGuid = ownerGuid
|
||||
)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
/* Copyright (c) 2014 Sanjay Dasgupta, All Rights Reserved
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*/
|
||||
|
||||
package sna
|
||||
|
||||
import com.sun.jna.{Function => JNAFunction}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.language.dynamics
|
||||
|
||||
class Library(val libName: String) extends Dynamic {
|
||||
|
||||
class Invocation(val jnaFunction: JNAFunction, val args: Array[Object]) {
|
||||
|
||||
// TODO: this does not call without a passed type parameter
|
||||
def apply[R](implicit m: Manifest[R]): R = {
|
||||
//println("invoking " + jnaFunction.getName + ". class " + m.runtimeClass.toString)
|
||||
if (m.runtimeClass == classOf[Unit]) {
|
||||
jnaFunction.invoke(args).asInstanceOf[R]
|
||||
} else {
|
||||
jnaFunction.invoke(m.runtimeClass, args).asInstanceOf[R]
|
||||
}
|
||||
}
|
||||
|
||||
def as[R](implicit m: Manifest[R]) = apply[R](m)
|
||||
def asInstanceOf[R](implicit m: Manifest[R]) = apply[R](m)
|
||||
}
|
||||
|
||||
def applyDynamic(functionName: String)(args: Any*) = {
|
||||
new Invocation(loadFunction(functionName), args.map(_.asInstanceOf[Object]).toArray[Object])
|
||||
}
|
||||
|
||||
private def loadFunction(functionName: String): JNAFunction = {
|
||||
var jnaFunction: JNAFunction = null
|
||||
if (functionCache.contains(functionName)) {
|
||||
jnaFunction = functionCache(functionName)
|
||||
} else {
|
||||
jnaFunction = JNAFunction.getFunction(libName, functionName)
|
||||
functionCache(functionName) = jnaFunction
|
||||
}
|
||||
|
||||
jnaFunction
|
||||
}
|
||||
|
||||
def prefetch(functionName: String): Unit = {
|
||||
loadFunction(functionName)
|
||||
}
|
||||
|
||||
private val functionCache = mutable.Map.empty[String, JNAFunction]
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.crypto.CryptoInterface.CryptoDHState
|
||||
import scodec.bits._
|
||||
|
||||
class CryptoInterfaceTest extends Specification {
|
||||
args(stopOnFail = true)
|
||||
"Crypto interface" should {
|
||||
"correctly initialize" in {
|
||||
CryptoInterface.initialize()
|
||||
ok
|
||||
}
|
||||
|
||||
"encrypt and decrypt" in {
|
||||
val key = hex"41414141"
|
||||
val plaintext = ByteVector.fill(16)(0x42)
|
||||
|
||||
val crypto = new CryptoInterface.CryptoState(key, key)
|
||||
|
||||
val ciphertext = crypto.encrypt(plaintext)
|
||||
val decrypted = crypto.decrypt(ciphertext)
|
||||
|
||||
crypto.close
|
||||
decrypted mustEqual plaintext
|
||||
ciphertext mustNotEqual plaintext
|
||||
}
|
||||
|
||||
"encrypt and decrypt must handle no bytes" in {
|
||||
val key = hex"41414141"
|
||||
val empty = ByteVector.empty
|
||||
|
||||
val crypto = new CryptoInterface.CryptoState(key, key)
|
||||
|
||||
val ciphertext = crypto.encrypt(empty)
|
||||
val decrypted = crypto.decrypt(ciphertext)
|
||||
|
||||
crypto.close
|
||||
|
||||
ciphertext mustEqual empty
|
||||
decrypted mustEqual empty
|
||||
}
|
||||
|
||||
"encrypt and decrypt must only accept block aligned inputs" in {
|
||||
val key = hex"41414141"
|
||||
val badPad = ByteVector.fill(CryptoInterface.RC5_BLOCK_SIZE - 1)('a')
|
||||
|
||||
val crypto = new CryptoInterface.CryptoState(key, key)
|
||||
|
||||
crypto.encrypt(badPad) must throwA[IllegalArgumentException]
|
||||
crypto.decrypt(badPad) must throwA[IllegalArgumentException]
|
||||
|
||||
crypto.close
|
||||
ok
|
||||
}
|
||||
|
||||
"arrive at a shared secret" in {
|
||||
val server = new CryptoInterface.CryptoDHState()
|
||||
val client = new CryptoInterface.CryptoDHState()
|
||||
|
||||
// 1. Client generates p, g, and its key pair
|
||||
client.start()
|
||||
|
||||
// 2. Client sends p and g to server who then generates a key pair as well
|
||||
server.start(client.getModulus, client.getGenerator)
|
||||
|
||||
// 3. Both parties come to a shared secret
|
||||
val clientAgreed = client.agree(server.getPublicKey)
|
||||
val serverAgreed = server.agree(client.getPublicKey)
|
||||
|
||||
// Free resources
|
||||
server.close
|
||||
client.close
|
||||
|
||||
clientAgreed mustEqual serverAgreed
|
||||
}
|
||||
|
||||
"must fail to agree on a secret with a bad public key" in {
|
||||
val server = new CryptoInterface.CryptoDHState()
|
||||
val client = new CryptoInterface.CryptoDHState()
|
||||
|
||||
// 1. Client generates p, g, and its key pair
|
||||
client.start()
|
||||
|
||||
// 2. Client sends p and g to server who then generates a key pair as well
|
||||
server.start(client.getModulus, client.getGenerator)
|
||||
|
||||
// 3. Client agrees with a bad public key, so it must fail
|
||||
val clientAgreed = client.agree(client.getPublicKey)
|
||||
val serverAgreed = server.agree(client.getPublicKey)
|
||||
|
||||
// Free resources
|
||||
server.close
|
||||
client.close
|
||||
|
||||
clientAgreed mustNotEqual serverAgreed
|
||||
}
|
||||
|
||||
"MD5MAC correctly" in {
|
||||
val key = hex"377b60f8790f91b35a9da82945743da9"
|
||||
val message = ByteVector(Array[Byte]('m', 'a', 's', 't', 'e', 'r', ' ', 's', 'e', 'c', 'r', 'e', 't')) ++
|
||||
hex"b4aea1559444a20b6112a2892de40eac00000000c8aea155b53d187076b79abab59001b600000000"
|
||||
val expected = hex"5aa15de41f5220cf5cca489155e1438c5aa15de4"
|
||||
|
||||
val output = CryptoInterface.MD5MAC(key, message, expected.length.toInt)
|
||||
|
||||
output mustEqual expected
|
||||
}
|
||||
|
||||
"safely handle multiple starts" in {
|
||||
val dontCare = ByteVector.fill(16)(0x42)
|
||||
val dh = new CryptoDHState()
|
||||
|
||||
dh.start()
|
||||
dh.start() must throwA[IllegalStateException]
|
||||
dh.close
|
||||
|
||||
ok
|
||||
}
|
||||
|
||||
"prevent function calls before initialization" in {
|
||||
val dontCare = ByteVector.fill(16)(0x42)
|
||||
val dh = new CryptoDHState()
|
||||
|
||||
dh.getGenerator must throwA[IllegalStateException]
|
||||
dh.getModulus must throwA[IllegalStateException]
|
||||
dh.getPrivateKey must throwA[IllegalStateException]
|
||||
dh.getPublicKey must throwA[IllegalStateException]
|
||||
dh.agree(dontCare) must throwA[IllegalStateException]
|
||||
dh.close
|
||||
|
||||
ok
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/test/scala/CryptoTest.scala
Normal file
106
src/test/scala/CryptoTest.scala
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
|
||||
import java.security.{SecureRandom, Security}
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.PacketCoding.CryptoCoding
|
||||
import net.psforever.packet.control.{HandleGamePacket, SlottedMetaPacket}
|
||||
import net.psforever.packet.game.PlanetsideAttributeMessage
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import net.psforever.util.{DiffieHellman, Md5Mac}
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import scodec.Attempt.Failure
|
||||
import scodec.Err
|
||||
import scodec.bits._
|
||||
|
||||
class CryptoTest extends Specification {
|
||||
Security.addProvider(new BouncyCastleProvider)
|
||||
|
||||
args(stopOnFail = true)
|
||||
"Crypto" should {
|
||||
"encrypt and decrypt" in {
|
||||
val key = hex"41414141414141414141414141414141"
|
||||
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
|
||||
val plaintext = ByteVector.fill(32)(0x42)
|
||||
|
||||
val crypto = CryptoCoding(keySpec, keySpec, key, key)
|
||||
|
||||
val ciphertext = crypto.encrypt(plaintext).require
|
||||
val decrypted = crypto.decrypt(ciphertext).require
|
||||
|
||||
decrypted mustEqual plaintext
|
||||
ciphertext mustNotEqual plaintext
|
||||
|
||||
}
|
||||
|
||||
"encrypt and decrypt must only accept block aligned inputs" in {
|
||||
val key = hex"41414141414141414141414141414141"
|
||||
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
|
||||
val badPad = ByteVector.fill(PacketCoding.RC5_BLOCK_SIZE - 1)('a')
|
||||
|
||||
val crypto = CryptoCoding(keySpec, keySpec, key, key)
|
||||
|
||||
//crypto.encrypt(badPad) must throwA[javax.crypto.IllegalBlockSizeException]
|
||||
crypto.decrypt(badPad) mustEqual Failure(Err("data not block size aligned"))
|
||||
}
|
||||
|
||||
"encrypt and decrypt packet" in {
|
||||
val key = hex"41414141414141414141414141414141"
|
||||
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
|
||||
val crypto = CryptoCoding(keySpec, keySpec, key, key)
|
||||
|
||||
val packet =
|
||||
SlottedMetaPacket(
|
||||
0,
|
||||
5,
|
||||
PacketCoding
|
||||
.encodePacket(
|
||||
HandleGamePacket(
|
||||
PacketCoding.encodePacket(PlanetsideAttributeMessage(PlanetSideGUID(0), 0, 0L)).require.toByteVector
|
||||
)
|
||||
)
|
||||
.require
|
||||
.bytes
|
||||
)
|
||||
|
||||
val encrypted = PacketCoding.marshalPacket(packet, Some(10), Some(crypto)).require
|
||||
println(s"encrypted ${encrypted}")
|
||||
val (decryptedPacket, sequence) = PacketCoding.unmarshalPacket(encrypted.bytes, Some(crypto)).require
|
||||
|
||||
decryptedPacket mustEqual packet
|
||||
sequence must beSome(10)
|
||||
}
|
||||
|
||||
"MD5MAC" in {
|
||||
val key = hex"377b60f8790f91b35a9da82945743da9"
|
||||
val message = ByteVector(Array[Byte]('m', 'a', 's', 't', 'e', 'r', ' ', 's', 'e', 'c', 'r', 'e', 't')) ++
|
||||
hex"b4aea1559444a20b6112a2892de40eac00000000c8aea155b53d187076b79abab59001b600000000"
|
||||
val message2 = ByteVector.view((0 until 64).map(_.toByte).toArray)
|
||||
val expected = hex"5aa15de41f5220cf5cca489155e1438c5aa15de4"
|
||||
|
||||
val mac = new Md5Mac(key)
|
||||
mac.update(message)
|
||||
mac.doFinal(20) mustEqual expected
|
||||
|
||||
val mac2 = new Md5Mac(key)
|
||||
mac2.update(message2)
|
||||
mac.update(message2)
|
||||
mac.doFinal() mustEqual mac2.doFinal()
|
||||
|
||||
(0 to 20).map(_ => mac.updateFinal(message, 20) mustEqual expected)
|
||||
}
|
||||
|
||||
"DH" in {
|
||||
val p = BigInt(128, new SecureRandom()).toByteArray
|
||||
val g = Array(1.toByte)
|
||||
|
||||
val bob = new DiffieHellman(p, g)
|
||||
val alice = new DiffieHellman(p, g)
|
||||
|
||||
bob.agree(alice.publicKey) mustEqual alice.agree(bob.publicKey)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.{PlanetSideControlPacket, _}
|
||||
import net.psforever.packet.control.{ClientStart, ServerStart}
|
||||
import scodec.bits._
|
||||
|
||||
|
|
@ -18,13 +18,13 @@ class PacketCodingTest extends Specification {
|
|||
|
||||
"Packet coding" should {
|
||||
"correctly decode control packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
val (packet, _) = PacketCoding.unmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
|
||||
packet.isInstanceOf[ControlPacket] mustEqual true
|
||||
packet.isInstanceOf[PlanetSideControlPacket] mustEqual true
|
||||
|
||||
val controlPacket = packet.asInstanceOf[ControlPacket]
|
||||
val controlPacket = packet.asInstanceOf[PlanetSideControlPacket]
|
||||
controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart
|
||||
controlPacket.packet mustEqual ClientStart(656287232)
|
||||
controlPacket mustEqual ClientStart(656287232)
|
||||
}
|
||||
|
||||
"encode and decode to identical packets" in {
|
||||
|
|
@ -32,37 +32,37 @@ class PacketCodingTest extends Specification {
|
|||
val serverNonce = 848483
|
||||
|
||||
val packetUnderTest = ServerStart(clientNonce, serverNonce)
|
||||
val pkt = PacketCoding.MarshalPacket(ControlPacket(packetUnderTest.opcode, packetUnderTest)).require
|
||||
val pkt = PacketCoding.marshalPacket(packetUnderTest).require
|
||||
|
||||
val decoded = PacketCoding.UnmarshalPacket(pkt.toByteVector).require.asInstanceOf[ControlPacket]
|
||||
val recvPkt = decoded.packet.asInstanceOf[ServerStart]
|
||||
val decoded = PacketCoding.unmarshalPacket(pkt.toByteVector).require._1.asInstanceOf[PlanetSideControlPacket]
|
||||
val recvPkt = decoded.asInstanceOf[ServerStart]
|
||||
|
||||
packetUnderTest mustEqual recvPkt
|
||||
}
|
||||
|
||||
"reject corrupted control packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00001002 00261e27 004101f0")
|
||||
val packet = PacketCoding.unmarshalPacket(hex"0001 00001002 00261e27 004101f0")
|
||||
|
||||
packet.isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"correctly decode crypto packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
val (packet, _) = PacketCoding.unmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
|
||||
packet.isInstanceOf[ControlPacket] mustEqual true
|
||||
packet.isInstanceOf[PlanetSideControlPacket] mustEqual true
|
||||
|
||||
val controlPacket = packet.asInstanceOf[ControlPacket]
|
||||
val controlPacket = packet.asInstanceOf[PlanetSideControlPacket]
|
||||
controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart
|
||||
controlPacket.packet mustEqual ClientStart(656287232)
|
||||
controlPacket mustEqual ClientStart(656287232)
|
||||
}
|
||||
|
||||
"reject bad packet types" in {
|
||||
PacketCoding.UnmarshalPacket(hex"ff414141").isFailure mustEqual true
|
||||
PacketCoding.unmarshalPacket(hex"ff414141").isFailure mustEqual true
|
||||
}
|
||||
|
||||
"reject small packets" in {
|
||||
PacketCoding.UnmarshalPacket(hex"00").isFailure mustEqual true
|
||||
PacketCoding.UnmarshalPacket(hex"").isFailure mustEqual true
|
||||
PacketCoding.unmarshalPacket(hex"00").isFailure mustEqual true
|
||||
PacketCoding.unmarshalPacket(hex"").isFailure mustEqual true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ClientStartTest extends Specification {
|
|||
val string = hex"0001 00000002 00261e27 000001f0"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ClientStart(nonce) =>
|
||||
nonce mustEqual 656287232
|
||||
case _ =>
|
||||
|
|
@ -20,7 +20,7 @@ class ClientStartTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ClientStart(656287232)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ConnectionCloseTest extends Specification {
|
|||
val string = hex"001D"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ConnectionClose() =>
|
||||
ok
|
||||
case _ =>
|
||||
|
|
@ -20,7 +20,7 @@ class ConnectionCloseTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ConnectionClose()
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ControlSyncRespTest extends Specification {
|
|||
val string = hex"0008 5268 21392D92 0000000000000276 0000000000000275 0000000000000275 0000000000000276"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ControlSyncResp(a, b, c, d, e, f) =>
|
||||
a mustEqual 21096
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class ControlSyncRespTest extends Specification {
|
|||
}
|
||||
|
||||
"encode" in {
|
||||
val encoded = PacketCoding.EncodePacket(ControlSyncResp(21096, 0x21392d92, 0x276, 0x275, 0x275, 0x276)).require
|
||||
val encoded = PacketCoding.encodePacket(ControlSyncResp(21096, 0x21392d92, 0x276, 0x275, 0x275, 0x276)).require
|
||||
|
||||
encoded.toByteVector mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ControlSyncTest extends Specification {
|
|||
val string = hex"0007 5268 0000004D 00000052 0000004D 0000007C 0000004D 0000000000000276 0000000000000275"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ControlSync(a, b, c, d, e, f, g, h) =>
|
||||
a mustEqual 21096
|
||||
b mustEqual 0x4d
|
||||
|
|
@ -26,7 +26,7 @@ class ControlSyncTest extends Specification {
|
|||
}
|
||||
|
||||
"encode" in {
|
||||
val encoded = PacketCoding.EncodePacket(ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)).require
|
||||
val encoded = PacketCoding.encodePacket(ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)).require
|
||||
encoded.toByteVector mustEqual string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class HandleGamePacketTest extends Specification {
|
|||
val string = hex"00 00 01 CB" ++ base
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case HandleGamePacket(len, data, extra) =>
|
||||
len mustEqual 459
|
||||
data mustEqual base
|
||||
|
|
@ -25,7 +25,7 @@ class HandleGamePacketTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val pkt = HandleGamePacket(base)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package control
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet.control.{ControlSync, MultiPacketBundle, MultiPacketCollector}
|
||||
import net.psforever.packet.crypto.{ClientFinished, ServerFinished}
|
||||
import net.psforever.packet.game.ObjectDeleteMessage
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
class MultiPacketCollectorTest extends Specification {
|
||||
val packet1 = ObjectDeleteMessage(PlanetSideGUID(1103), 2)
|
||||
|
||||
"MultiPacketBundle" should {
|
||||
import scodec.bits._
|
||||
val packet2 = ControlSync(21096, 0x4d, 0x52, 0x4d, 0x7c, 0x4d, 0x276, 0x275)
|
||||
|
||||
"construct" in {
|
||||
MultiPacketBundle(List(packet1))
|
||||
ok
|
||||
}
|
||||
|
||||
"fail to construct if not initialized with PlanetSidePackets" in {
|
||||
MultiPacketBundle(Nil) must throwA[IllegalArgumentException]
|
||||
}
|
||||
|
||||
"concatenate bundles into a new bundle" in {
|
||||
val obj1 = MultiPacketBundle(List(packet1))
|
||||
val obj2 = MultiPacketBundle(List(packet2))
|
||||
val obj3 = obj1 + obj2
|
||||
obj3 match {
|
||||
case MultiPacketBundle(list) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual packet1
|
||||
list(1) mustEqual packet2
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"accept PlanetSideGamePackets and PlanetSideControlPackets" in {
|
||||
MultiPacketBundle(List(packet2, packet1)) match {
|
||||
case MultiPacketBundle(list) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual packet2
|
||||
list(1) mustEqual packet1
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"ignore other types of PlanetSideContainerPackets" in {
|
||||
val param = List(packet2, ClientFinished(hex"", hex""), packet1, ServerFinished(hex""))
|
||||
MultiPacketBundle(param) match { //warning message will display in log
|
||||
case MultiPacketBundle(list) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual param.head
|
||||
list(1) mustEqual param(2)
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"MultiPacketCollector" should {
|
||||
val packet2 = ObjectDeleteMessage(PlanetSideGUID(1105), 2)
|
||||
val packet3 = ObjectDeleteMessage(PlanetSideGUID(1107), 2)
|
||||
|
||||
"construct" in {
|
||||
new MultiPacketCollector()
|
||||
ok
|
||||
}
|
||||
|
||||
"construct with initial packets" in {
|
||||
MultiPacketCollector(List(packet1, packet2))
|
||||
ok
|
||||
}
|
||||
|
||||
"can retrieve a bundle packets" in {
|
||||
val obj = MultiPacketCollector(List(packet1, packet2))
|
||||
obj.Bundle match {
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual packet1
|
||||
list(1) mustEqual packet2
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"can retrieve a bundle of potential packets" in {
|
||||
val obj1 = new MultiPacketCollector()
|
||||
obj1.Bundle match {
|
||||
case Some(_) =>
|
||||
ko
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
val obj2 = MultiPacketCollector(List(packet1, packet2))
|
||||
obj2.Bundle match {
|
||||
case None =>
|
||||
ko
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual packet1
|
||||
list(1) mustEqual packet2
|
||||
}
|
||||
}
|
||||
|
||||
"clear packets after being asked to bundle" in {
|
||||
val list = List(packet1, packet2)
|
||||
val obj = MultiPacketCollector(list)
|
||||
|
||||
obj.Bundle match {
|
||||
case Some(MultiPacketBundle(bundle)) =>
|
||||
bundle mustEqual list
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
|
||||
obj.Bundle match {
|
||||
case Some(MultiPacketBundle(_)) =>
|
||||
ko
|
||||
case _ =>
|
||||
ok
|
||||
}
|
||||
}
|
||||
|
||||
"add a packet" in {
|
||||
val obj = new MultiPacketCollector()
|
||||
obj.Add(packet1)
|
||||
obj.Bundle match {
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 1
|
||||
list.head mustEqual packet1
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"add packets" in {
|
||||
val obj = new MultiPacketCollector()
|
||||
obj.Add(List(packet1, packet2))
|
||||
obj.Bundle match {
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 2
|
||||
list.head mustEqual packet1
|
||||
list(1) mustEqual packet2
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"concatenate bundles (1)" in {
|
||||
|
||||
val obj1 = new MultiPacketCollector()
|
||||
obj1.Add(List(packet1, packet2))
|
||||
obj1.Bundle match {
|
||||
case Some(MultiPacketBundle(bundle1)) =>
|
||||
val obj2 = MultiPacketCollector(bundle1)
|
||||
obj2.Add(packet3)
|
||||
obj2.Bundle match {
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 3
|
||||
list.head mustEqual packet1
|
||||
list(1) mustEqual packet2
|
||||
list(2) mustEqual packet3
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"concatenate bundles (2)" in {
|
||||
val obj1 = new MultiPacketCollector()
|
||||
obj1.Add(List(packet1, packet2))
|
||||
obj1.Bundle match {
|
||||
case Some(MultiPacketBundle(bundle1)) =>
|
||||
val obj2 = new MultiPacketCollector()
|
||||
obj2.Add(packet3)
|
||||
obj2.Add(bundle1)
|
||||
obj2.Bundle match {
|
||||
case Some(MultiPacketBundle(list)) =>
|
||||
list.size mustEqual 3
|
||||
list.head mustEqual packet3
|
||||
list(1) mustEqual packet1
|
||||
list(2) mustEqual packet2
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ class MultiPacketTest extends Specification {
|
|||
hex"00 03 04 00 15 13 23 3A 00 09 03 E3 00 19 16 6D 56 05 68 05 40 A0 EF 45 00 15 0E 44 00 A0 A2 41 00 00 0F 88 00 06 E4 C0 60 00 00 00 15 E4 32 40 74 72 61 69 6E 69 6E 67 5F 77 65 61 70 6F 6E 73 30 31 13 BD 68 05 53 F6 EF 90 D1 6E 03 14 FE 78 8C 20 1C C0 00 00 1F 00 09 03 E4 6D 56 05 68 05 40 A0 EF 45 00 15 0E 44 30 89 A1 41 00 00 0F 8A 01 00 04 18 EF 80"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case MultiPacket(data) =>
|
||||
data.size mustEqual 4
|
||||
data(0) mustEqual hex"00151323"
|
||||
|
|
@ -34,7 +34,7 @@ class MultiPacketTest extends Specification {
|
|||
hex"000903e46d5605680540a0ef4500150e443089a14100000f8a01000418ef80"
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class RelatedATest extends Specification {
|
|||
val string3 = hex"00 14 01 04"
|
||||
|
||||
"decode (0)" in {
|
||||
PacketCoding.DecodePacket(string0).require match {
|
||||
PacketCoding.decodePacket(string0).require match {
|
||||
case RelatedA(slot, subslot) =>
|
||||
slot mustEqual 0
|
||||
subslot mustEqual 260
|
||||
|
|
@ -23,7 +23,7 @@ class RelatedATest extends Specification {
|
|||
}
|
||||
|
||||
"decode (1)" in {
|
||||
PacketCoding.DecodePacket(string1).require match {
|
||||
PacketCoding.decodePacket(string1).require match {
|
||||
case RelatedA(slot, subslot) =>
|
||||
slot mustEqual 1
|
||||
subslot mustEqual 260
|
||||
|
|
@ -33,7 +33,7 @@ class RelatedATest extends Specification {
|
|||
}
|
||||
|
||||
"decode (2)" in {
|
||||
PacketCoding.DecodePacket(string2).require match {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case RelatedA(slot, subslot) =>
|
||||
slot mustEqual 2
|
||||
subslot mustEqual 260
|
||||
|
|
@ -43,7 +43,7 @@ class RelatedATest extends Specification {
|
|||
}
|
||||
|
||||
"decode (3)" in {
|
||||
PacketCoding.DecodePacket(string3).require match {
|
||||
PacketCoding.decodePacket(string3).require match {
|
||||
case RelatedA(slot, subslot) =>
|
||||
slot mustEqual 3
|
||||
subslot mustEqual 260
|
||||
|
|
@ -54,25 +54,25 @@ class RelatedATest extends Specification {
|
|||
|
||||
"encode (0)" in {
|
||||
val pkt = RelatedA(0, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string0
|
||||
}
|
||||
|
||||
"encode (1)" in {
|
||||
val pkt = RelatedA(1, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string1
|
||||
}
|
||||
|
||||
"encode (2)" in {
|
||||
val pkt = RelatedA(2, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string2
|
||||
}
|
||||
|
||||
"encode (3)" in {
|
||||
val pkt = RelatedA(3, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string3
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class RelatedBTest extends Specification {
|
|||
val string3 = hex"00 18 01 04"
|
||||
|
||||
"decode (0)" in {
|
||||
PacketCoding.DecodePacket(string0).require match {
|
||||
PacketCoding.decodePacket(string0).require match {
|
||||
case RelatedB(slot, subslot) =>
|
||||
slot mustEqual 0
|
||||
subslot mustEqual 260
|
||||
|
|
@ -23,7 +23,7 @@ class RelatedBTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (1)" in {
|
||||
PacketCoding.DecodePacket(string1).require match {
|
||||
PacketCoding.decodePacket(string1).require match {
|
||||
case RelatedB(slot, subslot) =>
|
||||
slot mustEqual 1
|
||||
subslot mustEqual 260
|
||||
|
|
@ -33,7 +33,7 @@ class RelatedBTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (2)" in {
|
||||
PacketCoding.DecodePacket(string2).require match {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case RelatedB(slot, subslot) =>
|
||||
slot mustEqual 2
|
||||
subslot mustEqual 260
|
||||
|
|
@ -43,7 +43,7 @@ class RelatedBTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (3)" in {
|
||||
PacketCoding.DecodePacket(string3).require match {
|
||||
PacketCoding.decodePacket(string3).require match {
|
||||
case RelatedB(slot, subslot) =>
|
||||
slot mustEqual 3
|
||||
subslot mustEqual 260
|
||||
|
|
@ -54,25 +54,25 @@ class RelatedBTest extends Specification {
|
|||
|
||||
"encode (0)" in {
|
||||
val pkt = RelatedB(0, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string0
|
||||
}
|
||||
|
||||
"encode (1)" in {
|
||||
val pkt = RelatedB(1, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string1
|
||||
}
|
||||
|
||||
"encode (2)" in {
|
||||
val pkt = RelatedB(2, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string2
|
||||
}
|
||||
|
||||
"encode (3)" in {
|
||||
val pkt = RelatedB(3, 260)
|
||||
val msg = PacketCoding.EncodePacket(pkt).require.toByteVector
|
||||
val msg = PacketCoding.encodePacket(pkt).require.toByteVector
|
||||
msg mustEqual string3
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class SlottedMetaPacketTest extends Specification {
|
|||
.toByteVector ++ uint16.encode(subslot).require.toByteVector ++ rest
|
||||
|
||||
"decode as the base slot and subslot" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case SlottedMetaPacket(slot, subslot, rest) =>
|
||||
slot mustEqual 0
|
||||
subslot mustEqual 0
|
||||
|
|
@ -48,7 +48,7 @@ class SlottedMetaPacketTest extends Specification {
|
|||
val subslot = 12323
|
||||
val pkt = createMetaPacket(i, subslot, ByteVector.empty)
|
||||
|
||||
PacketCoding.DecodePacket(pkt).require match {
|
||||
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
|
||||
|
|
@ -64,16 +64,16 @@ class SlottedMetaPacketTest extends Specification {
|
|||
}
|
||||
|
||||
"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
|
||||
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]
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class TeardownConnectionTest extends Specification {
|
|||
val string = hex"00 05 02 4F 57 17 00 06"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case TeardownConnection(nonce) =>
|
||||
nonce mustEqual 391597826
|
||||
case _ =>
|
||||
|
|
@ -19,7 +19,7 @@ class TeardownConnectionTest extends Specification {
|
|||
}
|
||||
|
||||
"encode" in {
|
||||
val encoded = PacketCoding.EncodePacket(TeardownConnection(391597826)).require
|
||||
val encoded = PacketCoding.encodePacket(TeardownConnection(391597826)).require
|
||||
|
||||
encoded.toByteVector mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ActionCancelMessageTest extends Specification {
|
|||
val string = hex"22 201ee01a10"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ActionCancelMessage(player_guid, object_guid, unk) =>
|
||||
player_guid mustEqual PlanetSideGUID(7712)
|
||||
object_guid mustEqual PlanetSideGUID(6880)
|
||||
|
|
@ -23,7 +23,7 @@ class ActionCancelMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ActionCancelMessage(PlanetSideGUID(7712), PlanetSideGUID(6880), 1)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ActionProgressMessageTest extends Specification {
|
|||
val string = hex"216000000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ActionProgressMessage(unk1, unk2) =>
|
||||
unk1 mustEqual 6
|
||||
unk2 mustEqual 0
|
||||
|
|
@ -21,7 +21,7 @@ class ActionProgressMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ActionProgressMessage(6, 0L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ActionResultMessageTest extends Specification {
|
|||
val string_fail = hex"1f 0080000000"
|
||||
|
||||
"decode (pass)" in {
|
||||
PacketCoding.DecodePacket(string_pass).require match {
|
||||
PacketCoding.decodePacket(string_pass).require match {
|
||||
case ActionResultMessage(okay, code) =>
|
||||
okay mustEqual true
|
||||
code mustEqual None
|
||||
|
|
@ -21,7 +21,7 @@ class ActionResultMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (fail)" in {
|
||||
PacketCoding.DecodePacket(string_fail).require match {
|
||||
PacketCoding.decodePacket(string_fail).require match {
|
||||
case ActionResultMessage(okay, code) =>
|
||||
okay mustEqual false
|
||||
code mustEqual Some(1)
|
||||
|
|
@ -32,28 +32,28 @@ class ActionResultMessageTest extends Specification {
|
|||
|
||||
"encode (pass, full)" in {
|
||||
val msg = ActionResultMessage(true, None)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_pass
|
||||
}
|
||||
|
||||
"encode (pass, minimal)" in {
|
||||
val msg = ActionResultMessage.Pass
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_pass
|
||||
}
|
||||
|
||||
"encode (fail, full)" in {
|
||||
val msg = ActionResultMessage(false, Some(1))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_fail
|
||||
}
|
||||
|
||||
"encode (fail, minimal)" in {
|
||||
val msg = ActionResultMessage.Fail(1)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_fail
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ class AggravatedDamageMessageTest extends Specification {
|
|||
val string = hex"6a350a0e000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case AggravatedDamageMessage(guid,unk) =>
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AggravatedDamageMessage(guid, unk) =>
|
||||
guid mustEqual PlanetSideGUID(2613)
|
||||
unk mustEqual 14
|
||||
case _ =>
|
||||
|
|
@ -22,7 +22,7 @@ class AggravatedDamageMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AggravatedDamageMessage(PlanetSideGUID(2613), 14)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ArmorChangedMessageTest extends Specification {
|
|||
val string = hex"3E 11 01 4C"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ArmorChangedMessage(player_guid, armor, subtype) =>
|
||||
player_guid mustEqual PlanetSideGUID(273)
|
||||
armor mustEqual ExoSuitType.MAX
|
||||
|
|
@ -23,7 +23,7 @@ class ArmorChangedMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ArmorChangedMessage(PlanetSideGUID(273), ExoSuitType.MAX, 3)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class AvatarDeadStateMessageTest extends Specification {
|
|||
val string_invalid = hex"ad3c1260801c12608009f99861fb0741e0400000F0"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarDeadStateMessage(unk1, unk2, unk3, pos, unk4, unk5) =>
|
||||
unk1 mustEqual DeadState.Dead
|
||||
unk2 mustEqual 300000
|
||||
|
|
@ -26,7 +26,7 @@ class AvatarDeadStateMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (failure)" in {
|
||||
PacketCoding.DecodePacket(string_invalid).isFailure mustEqual true
|
||||
PacketCoding.decodePacket(string_invalid).isFailure mustEqual true
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
|
|
@ -38,7 +38,7 @@ class AvatarDeadStateMessageTest extends Specification {
|
|||
PlanetSideEmpire.VS,
|
||||
true
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AvatarFirstTimeEventMessageTest extends Specification {
|
|||
val string = hex"69 4b00 c000 01000000 9e 766973697465645f63657274696669636174696f6e5f7465726d696e616c"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarFirstTimeEventMessage(avatar_guid, object_guid, unk1, event_name) =>
|
||||
avatar_guid mustEqual PlanetSideGUID(75)
|
||||
object_guid mustEqual PlanetSideGUID(192)
|
||||
|
|
@ -24,7 +24,7 @@ class AvatarFirstTimeEventMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AvatarFirstTimeEventMessage(PlanetSideGUID(75), PlanetSideGUID(192), 1, "visited_certification_terminal")
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AvatarGrenadeStateMessageTest extends Specification {
|
|||
val string = hex"A9 DA11 01"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarGrenadeStateMessage(player_guid, state) =>
|
||||
player_guid mustEqual PlanetSideGUID(4570)
|
||||
state mustEqual GrenadeState.Primed
|
||||
|
|
@ -22,7 +22,7 @@ class AvatarGrenadeStateMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.Primed)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AvatarImplantMessageTest extends Specification {
|
|||
val string = hex"58 630C 68 80"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarImplantMessage(player_guid, unk1, unk2, implant) =>
|
||||
player_guid mustEqual PlanetSideGUID(3171)
|
||||
unk1 mustEqual ImplantAction.Activation
|
||||
|
|
@ -24,7 +24,7 @@ class AvatarImplantMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AvatarImplantMessage(PlanetSideGUID(3171), ImplantAction.Activation, 1, 1)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class AvatarJumpMessageTest extends Specification {
|
|||
val string = hex"35 80"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarJumpMessage(state) =>
|
||||
state mustEqual true
|
||||
case _ =>
|
||||
|
|
@ -20,7 +20,7 @@ class AvatarJumpMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AvatarJumpMessage(true)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AvatarSearchCriteriaMessageTest extends Specification {
|
|||
val string = hex"64 C604 00 00 00 00 00 00"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarSearchCriteriaMessage(unk1, unk2) =>
|
||||
unk1 mustEqual PlanetSideGUID(1222)
|
||||
unk2.length mustEqual 6
|
||||
|
|
@ -28,23 +28,23 @@ class AvatarSearchCriteriaMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = AvatarSearchCriteriaMessage(PlanetSideGUID(1222), List(0, 0, 0, 0, 0, 0))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
||||
"encode (failure; wrong number of list entries)" in {
|
||||
val msg = AvatarSearchCriteriaMessage(PlanetSideGUID(1222), List(0))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"encode (failure; list number too big)" in {
|
||||
val msg = AvatarSearchCriteriaMessage(PlanetSideGUID(1222), List(0, 0, 0, 0, 0, 256))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"encode (failure; list number too small)" in {
|
||||
val msg = AvatarSearchCriteriaMessage(PlanetSideGUID(1222), List(0, 0, 0, -1, 0, 0))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class AvatarStatisticsMessageTest extends Specification {
|
|||
hex"7F 01 3C 40 20 00 00 00 C0 00 00 00 00 00 00 00 20 00 00 00 20 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00"
|
||||
|
||||
"decode (long)" in {
|
||||
PacketCoding.DecodePacket(string_long).require match {
|
||||
PacketCoding.decodePacket(string_long).require match {
|
||||
case AvatarStatisticsMessage(unk, stats) =>
|
||||
unk mustEqual 2
|
||||
stats.unk1 mustEqual None
|
||||
|
|
@ -25,7 +25,7 @@ class AvatarStatisticsMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (complex)" in {
|
||||
PacketCoding.DecodePacket(string_complex).require match {
|
||||
PacketCoding.decodePacket(string_complex).require match {
|
||||
case AvatarStatisticsMessage(unk, stats) =>
|
||||
unk mustEqual 0
|
||||
stats.unk1 mustEqual Some(1)
|
||||
|
|
@ -46,35 +46,35 @@ class AvatarStatisticsMessageTest extends Specification {
|
|||
|
||||
"encode (long)" in {
|
||||
val msg = AvatarStatisticsMessage(2, Statistics(0L))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_long
|
||||
}
|
||||
|
||||
"encode (complex)" in {
|
||||
val msg = AvatarStatisticsMessage(0, Statistics(1, 572, List[Long](1, 6, 0, 1, 1, 2, 0, 0)))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_complex
|
||||
}
|
||||
|
||||
"encode (failure; long; missing value)" in {
|
||||
val msg = AvatarStatisticsMessage(0, Statistics(None, None, List(0L)))
|
||||
PacketCoding.EncodePacket(msg).isFailure mustEqual true
|
||||
PacketCoding.encodePacket(msg).isFailure mustEqual true
|
||||
}
|
||||
|
||||
"encode (failure; complex; missing value (5-bit))" in {
|
||||
val msg = AvatarStatisticsMessage(0, Statistics(None, Some(572), List[Long](1, 6, 0, 1, 1, 2, 0, 0)))
|
||||
PacketCoding.EncodePacket(msg).isFailure mustEqual true
|
||||
PacketCoding.encodePacket(msg).isFailure mustEqual true
|
||||
}
|
||||
|
||||
"encode (failure; complex; missing value (11-bit))" in {
|
||||
val msg = AvatarStatisticsMessage(0, Statistics(Some(1), None, List[Long](1, 6, 0, 1, 1, 2, 0, 0)))
|
||||
PacketCoding.EncodePacket(msg).isFailure mustEqual true
|
||||
PacketCoding.encodePacket(msg).isFailure mustEqual true
|
||||
}
|
||||
|
||||
"encode (failure; complex; wrong number of list entries)" in {
|
||||
val msg = AvatarStatisticsMessage(0, Statistics(Some(1), None, List[Long](1, 6, 0, 1)))
|
||||
PacketCoding.EncodePacket(msg).isFailure mustEqual true
|
||||
PacketCoding.encodePacket(msg).isFailure mustEqual true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class AvatarVehicleTimerMessageTest extends Specification {
|
|||
val string2 = hex"57971b84667572794800000080"
|
||||
|
||||
"decode medkit" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case AvatarVehicleTimerMessage(player_guid, text, time, u1) =>
|
||||
player_guid mustEqual PlanetSideGUID(5821)
|
||||
text mustEqual "medkit"
|
||||
|
|
@ -23,7 +23,7 @@ class AvatarVehicleTimerMessageTest extends Specification {
|
|||
}
|
||||
}
|
||||
"decode fury" in {
|
||||
PacketCoding.DecodePacket(string2).require match {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case AvatarVehicleTimerMessage(player_guid, text, time, u1) =>
|
||||
player_guid mustEqual PlanetSideGUID(7063)
|
||||
text mustEqual "fury"
|
||||
|
|
@ -36,13 +36,13 @@ class AvatarVehicleTimerMessageTest extends Specification {
|
|||
|
||||
"encode medkit" in {
|
||||
val msg = AvatarVehicleTimerMessage(PlanetSideGUID(5821), "medkit", 5, false)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
"encode fury" in {
|
||||
val msg = AvatarVehicleTimerMessage(PlanetSideGUID(7063), "fury", 72, true)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class BattleExperienceMessageTest extends Specification {
|
|||
val string = hex"B4 8A0A E7030000 00"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case BattleExperienceMessage(player_guid, experience, unk) =>
|
||||
player_guid mustEqual PlanetSideGUID(2698)
|
||||
experience mustEqual 999
|
||||
|
|
@ -23,7 +23,7 @@ class BattleExperienceMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = BattleExperienceMessage(PlanetSideGUID(2698), 999, 0)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class BattleplanMessageTest extends Specification {
|
|||
//0xb3856477028c4f0075007400730074006100620075006c006f00750073000a000130
|
||||
|
||||
"decode (start)" in {
|
||||
PacketCoding.DecodePacket(string_start).require match {
|
||||
PacketCoding.decodePacket(string_start).require match {
|
||||
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
|
||||
char_id mustEqual 41490746
|
||||
player_name mustEqual "YetAnotherFailureAlt"
|
||||
|
|
@ -34,7 +34,7 @@ class BattleplanMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (end)" in {
|
||||
PacketCoding.DecodePacket(string_stop).require match {
|
||||
PacketCoding.decodePacket(string_stop).require match {
|
||||
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
|
||||
char_id mustEqual 41490746
|
||||
player_name mustEqual "YetAnotherFailureAlt"
|
||||
|
|
@ -49,7 +49,7 @@ class BattleplanMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (stop)" in {
|
||||
PacketCoding.DecodePacket(string_line).require match {
|
||||
PacketCoding.decodePacket(string_line).require match {
|
||||
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
|
||||
char_id mustEqual 41378949
|
||||
player_name mustEqual "Outstabulous"
|
||||
|
|
@ -191,7 +191,7 @@ class BattleplanMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (style)" in {
|
||||
PacketCoding.DecodePacket(string_style).require match {
|
||||
PacketCoding.decodePacket(string_style).require match {
|
||||
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
|
||||
char_id mustEqual 41378949
|
||||
player_name mustEqual "Outstabulous"
|
||||
|
|
@ -217,7 +217,7 @@ class BattleplanMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (message)" in {
|
||||
PacketCoding.DecodePacket(string_message).require match {
|
||||
PacketCoding.decodePacket(string_message).require match {
|
||||
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
|
||||
char_id mustEqual 41378949
|
||||
player_name mustEqual "Outstabulous"
|
||||
|
|
@ -245,7 +245,7 @@ class BattleplanMessageTest extends Specification {
|
|||
BattleDiagramAction(DiagramActionCode.StartDrawing) ::
|
||||
Nil
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_start
|
||||
}
|
||||
|
|
@ -258,7 +258,7 @@ class BattleplanMessageTest extends Specification {
|
|||
BattleDiagramAction(DiagramActionCode.StopDrawing) ::
|
||||
Nil
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_stop
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ class BattleplanMessageTest extends Specification {
|
|||
BattleDiagramAction.vertex(7536.0f, 6632.0f) ::
|
||||
Nil
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_line
|
||||
}
|
||||
|
|
@ -317,7 +317,7 @@ class BattleplanMessageTest extends Specification {
|
|||
BattleDiagramAction.vertex(7512.0f, 6344.0f) ::
|
||||
Nil
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_style
|
||||
}
|
||||
|
|
@ -329,7 +329,7 @@ class BattleplanMessageTest extends Specification {
|
|||
10,
|
||||
BattleDiagramAction.drawString(7512.0f, 6312.0f, 2, 0, "Hello Auraxis!") :: Nil
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_message
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class BeginZoningMessageTest extends Specification {
|
|||
val string = hex"43" //yes, just the opcode
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case BeginZoningMessage() =>
|
||||
ok
|
||||
case _ =>
|
||||
|
|
@ -20,7 +20,7 @@ class BeginZoningMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = BeginZoningMessage()
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
val string_akkan = hex"16048440616d7388100000001400000214e171a8e33024"
|
||||
|
||||
"decode (standard)" in {
|
||||
PacketCoding.DecodePacket(string_standard).require match {
|
||||
PacketCoding.decodePacket(string_standard).require match {
|
||||
case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
||||
action mustEqual BindStatus.Unbind
|
||||
bindDesc mustEqual ""
|
||||
|
|
@ -30,7 +30,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (ams)" in {
|
||||
PacketCoding.DecodePacket(string_ams).require match {
|
||||
PacketCoding.decodePacket(string_ams).require match {
|
||||
case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
||||
action mustEqual BindStatus.Unavailable
|
||||
bindDesc mustEqual "@ams"
|
||||
|
|
@ -46,7 +46,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (tech)" in {
|
||||
PacketCoding.DecodePacket(string_tech).require match {
|
||||
PacketCoding.decodePacket(string_tech).require match {
|
||||
case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
||||
action mustEqual BindStatus.Bind
|
||||
bindDesc mustEqual "@tech_plant"
|
||||
|
|
@ -62,7 +62,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (akkan)" in {
|
||||
PacketCoding.DecodePacket(string_akkan).require match {
|
||||
PacketCoding.decodePacket(string_akkan).require match {
|
||||
case BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
||||
action mustEqual BindStatus.Available
|
||||
bindDesc mustEqual "@ams"
|
||||
|
|
@ -79,14 +79,14 @@ class BindPlayerMessageTest extends Specification {
|
|||
|
||||
"encode (standard)" in {
|
||||
val msg = BindPlayerMessage.Standard
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_standard
|
||||
}
|
||||
|
||||
"encode (ams)" in {
|
||||
val msg = BindPlayerMessage(BindStatus.Unavailable, "@ams", false, false, SpawnGroup.AMS, 10, 0, Vector3.Zero)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_ams
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
14,
|
||||
Vector3(4610.0f, 6292, 69.625f)
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_tech
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ class BindPlayerMessageTest extends Specification {
|
|||
5,
|
||||
Vector3(2673.039f, 4423.547f, 39.1875f)
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_akkan
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class BroadcastWarpgateUpdateMessageTest extends Specification {
|
|||
val string = hex"D9 0D 00 01 00 20"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case BroadcastWarpgateUpdateMessage(continent_guid, building_guid, state1, state2, state3) =>
|
||||
continent_guid mustEqual 13
|
||||
building_guid mustEqual 1
|
||||
|
|
@ -24,7 +24,7 @@ class BroadcastWarpgateUpdateMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = BroadcastWarpgateUpdateMessage(13, 1, false, false, true)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class BugReportMessageTest extends Specification {
|
|||
hex"89 03000000 0F000000 8B4465632020322032303039 1 1 0 19 6C511 656B1 7A11 830610062006300 843100320033003400"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case BugReportMessage(major, minor, date, btype, repeat, unk, zone, loc, summary, desc) =>
|
||||
major mustEqual 3
|
||||
minor mustEqual 15
|
||||
|
|
@ -43,7 +43,7 @@ class BugReportMessageTest extends Specification {
|
|||
"abc",
|
||||
"1234"
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
|||
val string = hex"a0 04 00 09 00 16 00 00 00 00 80 00 00 00 17 00 00 00 00 00 00 40"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case BuildingInfoUpdateMessage(
|
||||
continent_guid,
|
||||
building_guid,
|
||||
|
|
@ -86,7 +86,7 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
|||
false,
|
||||
false
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class ChainLashMessageTest extends Specification {
|
|||
val string2 = hex"c5 5282e910100000093050"
|
||||
|
||||
"decode (1)" in {
|
||||
PacketCoding.DecodePacket(string1).require match {
|
||||
PacketCoding.decodePacket(string1).require match {
|
||||
case ChainLashMessage(u1a, u1b, u2, u3) =>
|
||||
u1a.isEmpty mustEqual true
|
||||
u1b.contains(Vector3(7673.164f, 544.1328f, 14.984375f)) mustEqual true
|
||||
|
|
@ -24,7 +24,7 @@ class ChainLashMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (2)" in {
|
||||
PacketCoding.DecodePacket(string2).require match {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case ChainLashMessage(u1a, u1b, u2, u3) =>
|
||||
u1a.contains(PlanetSideGUID(1445)) mustEqual true
|
||||
u1b.isEmpty mustEqual true
|
||||
|
|
@ -37,14 +37,14 @@ class ChainLashMessageTest extends Specification {
|
|||
|
||||
"encode (1)" in {
|
||||
val msg = ChainLashMessage(Vector3(7673.164f, 544.1328f, 14.984375f), 466, List(PlanetSideGUID(1603)))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string1
|
||||
}
|
||||
|
||||
"encode (2)" in {
|
||||
val msg = ChainLashMessage(PlanetSideGUID(1445), 466, List(PlanetSideGUID(1427)))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChangeAmmoMessageTest extends Specification {
|
|||
val string = hex"47 4E00 00000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChangeAmmoMessage(item_guid, unk1) =>
|
||||
item_guid mustEqual PlanetSideGUID(78)
|
||||
unk1 mustEqual 0
|
||||
|
|
@ -22,7 +22,7 @@ class ChangeAmmoMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChangeAmmoMessage(PlanetSideGUID(78), 0)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChangeFireModeMessageTest extends Specification {
|
|||
val string = hex"46 4C0020"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChangeFireModeMessage(item_guid, fire_mode) =>
|
||||
item_guid mustEqual PlanetSideGUID(76)
|
||||
fire_mode mustEqual 1
|
||||
|
|
@ -22,7 +22,7 @@ class ChangeFireModeMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChangeFireModeMessage(PlanetSideGUID(76), 1)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChangeFireStateMessage_StartTest extends Specification {
|
|||
val string = hex"39 4C00"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChangeFireStateMessage_Start(item_guid) =>
|
||||
item_guid mustEqual PlanetSideGUID(76)
|
||||
case _ =>
|
||||
|
|
@ -21,7 +21,7 @@ class ChangeFireStateMessage_StartTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChangeFireStateMessage_Start(PlanetSideGUID(76))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChangeFireStateMessage_StopTest extends Specification {
|
|||
val string = hex"3A 4C00"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChangeFireStateMessage_Stop(item_guid) =>
|
||||
item_guid mustEqual PlanetSideGUID(76)
|
||||
case _ =>
|
||||
|
|
@ -21,7 +21,7 @@ class ChangeFireStateMessage_StopTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChangeFireStateMessage_Stop(PlanetSideGUID(76))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChangeShortcutBankMessageTest extends Specification {
|
|||
val string = hex"29 4B00 20"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChangeShortcutBankMessage(player_guid, bank) =>
|
||||
player_guid mustEqual PlanetSideGUID(75)
|
||||
bank mustEqual 2
|
||||
|
|
@ -22,7 +22,7 @@ class ChangeShortcutBankMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChangeShortcutBankMessage(PlanetSideGUID(75), 2)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class CharacterCreateRequestMessageTest extends Specification {
|
|||
val string = hex"2f 88 54006500730074004300680061007200 320590"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case CharacterCreateRequestMessage(name, head, voice, gender, faction) =>
|
||||
name mustEqual "TestChar"
|
||||
head mustEqual 50
|
||||
|
|
@ -26,7 +26,7 @@ class CharacterCreateRequestMessageTest extends Specification {
|
|||
"encode" in {
|
||||
val msg =
|
||||
CharacterCreateRequestMessage("TestChar", 50, CharacterVoice.Voice5, CharacterGender.Female, PlanetSideEmpire.NC)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class CharacterInfoMessageTest extends Specification {
|
|||
val string = hex"14 0F000000 10270000C1D87A024B00265CB08000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case CharacterInfoMessage(unk, zone, charId, guid, finished, last) =>
|
||||
unk mustEqual 15L
|
||||
zone mustEqual PlanetSideZoneID(10000)
|
||||
|
|
@ -26,7 +26,7 @@ class CharacterInfoMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = CharacterInfoMessage(15L, PlanetSideZoneID(10000), 41605313L, PlanetSideGUID(75), false, 6404428L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CharacterKnowledgeMessageTest extends Specification {
|
|||
val string = hex"ec cc637a02 45804600720061006e006b0065006e00740061006e006b0003c022dc0008f01800"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case CharacterKnowledgeMessage(char_id, Some(info)) =>
|
||||
char_id mustEqual 41575372L
|
||||
info mustEqual CharacterKnowledgeInfo(
|
||||
|
|
@ -64,7 +64,7 @@ class CharacterKnowledgeMessageTest extends Specification {
|
|||
PlanetSideGUID(12)
|
||||
)
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class CharacterNoRecordMessageTest extends Specification {
|
|||
val string = hex"13 00400000" //we have no record of this packet, so here's something fake that works
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case CharacterNoRecordMessage(unk) =>
|
||||
unk mustEqual 16384
|
||||
case _ =>
|
||||
|
|
@ -20,7 +20,7 @@ class CharacterNoRecordMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = CharacterNoRecordMessage(16384)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class CharacterRequestMessageTest extends Specification {
|
|||
val string = hex"30 c1d87a02 00000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case CharacterRequestMessage(charId, action) =>
|
||||
charId mustEqual 41605313L
|
||||
action mustEqual CharacterRequestAction.Select
|
||||
|
|
@ -21,7 +21,7 @@ class CharacterRequestMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = CharacterRequestMessage(41605313L, CharacterRequestAction.Select)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class ChatMsgTest extends Specification {
|
|||
val string_tell = hex"12 20 C180640065006600 83610062006300"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string_local).require match {
|
||||
PacketCoding.decodePacket(string_local).require match {
|
||||
case ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
|
||||
messagetype mustEqual ChatMessageType.CMT_OPEN
|
||||
has_wide_contents mustEqual true
|
||||
|
|
@ -23,7 +23,7 @@ class ChatMsgTest extends Specification {
|
|||
ko
|
||||
}
|
||||
|
||||
PacketCoding.DecodePacket(string_tell).require match {
|
||||
PacketCoding.decodePacket(string_tell).require match {
|
||||
case ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
|
||||
messagetype mustEqual ChatMessageType.CMT_TELL
|
||||
has_wide_contents mustEqual true
|
||||
|
|
@ -37,12 +37,12 @@ class ChatMsgTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg_local = ChatMsg(ChatMessageType.CMT_OPEN, true, "", "abc", None)
|
||||
val pkt_local = PacketCoding.EncodePacket(msg_local).require.toByteVector
|
||||
val pkt_local = PacketCoding.encodePacket(msg_local).require.toByteVector
|
||||
|
||||
pkt_local mustEqual string_local
|
||||
|
||||
val msg_tell = ChatMsg(ChatMessageType.CMT_TELL, true, "def", "abc", None)
|
||||
val pkt_tell = PacketCoding.EncodePacket(msg_tell).require.toByteVector
|
||||
val pkt_tell = PacketCoding.encodePacket(msg_tell).require.toByteVector
|
||||
|
||||
pkt_tell mustEqual string_tell
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ChildObjectStateMessageTest extends Specification {
|
|||
val string = hex"1E 640B 06 47"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ChildObjectStateMessage(object_guid, pitch, yaw) =>
|
||||
object_guid mustEqual PlanetSideGUID(2916)
|
||||
pitch mustEqual 343.125f
|
||||
|
|
@ -23,7 +23,7 @@ class ChildObjectStateMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ChildObjectStateMessage(PlanetSideGUID(2916), 343.125f, 160.3125f)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ConnectToWorldMessageTest extends Specification {
|
|||
val string = hex"04 8667656D696E69 8C36342E33372E3135382E36393C75"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ConnectToWorldMessage(serverName, serverIp, serverPort) =>
|
||||
serverName mustEqual "gemini"
|
||||
serverIp mustEqual "64.37.158.69"
|
||||
|
|
@ -22,7 +22,7 @@ class ConnectToWorldMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ConnectToWorldMessage("gemini", "64.37.158.69", 30012)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ConnectToWorldRequestMessageTest extends Specification {
|
|||
hex"03 8667656D696E69 0000000000000000 00000000 00000000 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 "
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ConnectToWorldRequestMessage(serverName, token, majorVersion, minorVersion, revision, buildDate, unk) =>
|
||||
serverName mustEqual "gemini"
|
||||
token mustEqual ""
|
||||
|
|
@ -27,7 +27,7 @@ class ConnectToWorldRequestMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ConnectToWorldRequestMessage("gemini", "", 0, 0, 0, "", 0)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ContinentalLockUpdateMessageTest extends Specification {
|
|||
val string = hex"A8 16 00 40"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case ContinentalLockUpdateMessage(continent_guid, empire) =>
|
||||
continent_guid mustEqual 22
|
||||
empire mustEqual PlanetSideEmpire.NC
|
||||
|
|
@ -22,7 +22,7 @@ class ContinentalLockUpdateMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = ContinentalLockUpdateMessage(22, PlanetSideEmpire.NC)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
val stringRemove = hex"28 4C05 01 00 00"
|
||||
|
||||
"decode (medkit)" in {
|
||||
PacketCoding.DecodePacket(stringMedkit).require match {
|
||||
PacketCoding.decodePacket(stringMedkit).require match {
|
||||
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
|
||||
player_guid mustEqual PlanetSideGUID(4210)
|
||||
slot mustEqual 1
|
||||
|
|
@ -31,7 +31,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (macro)" in {
|
||||
PacketCoding.DecodePacket(stringMacro).require match {
|
||||
PacketCoding.decodePacket(stringMacro).require match {
|
||||
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
|
||||
player_guid mustEqual PlanetSideGUID(1356)
|
||||
slot mustEqual 8
|
||||
|
|
@ -48,7 +48,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (remove)" in {
|
||||
PacketCoding.DecodePacket(stringRemove).require match {
|
||||
PacketCoding.decodePacket(stringRemove).require match {
|
||||
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
|
||||
player_guid mustEqual PlanetSideGUID(1356)
|
||||
slot mustEqual 1
|
||||
|
|
@ -62,7 +62,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
|
||||
"encode (medkit)" in {
|
||||
val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, 0, true, Some(Shortcut(0, "medkit")))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual stringMedkit
|
||||
}
|
||||
|
|
@ -75,14 +75,14 @@ class CreateShortcutMessageTest extends Specification {
|
|||
true,
|
||||
Some(Shortcut(1, "shortcut_macro", "NTU", "/platoon Incoming NTU spam!"))
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual stringMacro
|
||||
}
|
||||
|
||||
"encode (remove)" in {
|
||||
val msg = CreateShortcutMessage(PlanetSideGUID(1356), 1, 0, false)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual stringRemove
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class DamageFeedbackMessageTest extends Specification {
|
|||
val string_2 = hex"7B 5E5826D8001DC0400000"
|
||||
|
||||
"decode (string 1)" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DamageFeedbackMessage(unk1, unk2, unk2a, unk2b, unk2c, unk3, unk3a, unk3b, unk3c, unk3d, unk4, unk5, unk6) =>
|
||||
unk1 mustEqual 3
|
||||
unk2 mustEqual true
|
||||
|
|
@ -33,7 +33,7 @@ class DamageFeedbackMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"decode (string 2)" in {
|
||||
PacketCoding.DecodePacket(string_2).require match {
|
||||
PacketCoding.decodePacket(string_2).require match {
|
||||
case DamageFeedbackMessage(unk1, unk2, unk2a, unk2b, unk2c, unk3, unk3a, unk3b, unk3c, unk3d, unk4, unk5, unk6) =>
|
||||
unk1 mustEqual 5
|
||||
unk2 mustEqual true
|
||||
|
|
@ -69,7 +69,7 @@ class DamageFeedbackMessageTest extends Specification {
|
|||
2,
|
||||
0
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ class DamageFeedbackMessageTest extends Specification {
|
|||
750,
|
||||
0
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string_2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DamageMessageTest extends Specification {
|
|||
val string = hex"0b610b02610b00"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DamageMessage(guid1, unk1, guid2, unk2) =>
|
||||
guid1 mustEqual PlanetSideGUID(2913)
|
||||
unk1 mustEqual 2
|
||||
|
|
@ -24,7 +24,7 @@ class DamageMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DamageMessage(PlanetSideGUID(2913), 2, PlanetSideGUID(2913), false)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DamageWithPositionMessageTest extends Specification {
|
|||
val string = hex"A6 11 6C2D7 65535 CA16"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DamageWithPositionMessage(unk, pos) =>
|
||||
unk mustEqual 17
|
||||
pos.x mustEqual 3674.8438f
|
||||
|
|
@ -24,7 +24,7 @@ class DamageWithPositionMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DamageWithPositionMessage(17, Vector3(3674.8438f, 2726.789f, 91.15625f))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class DataChallengeMessageRespTest extends Specification {
|
|||
val string = hex"948673616d706c6501000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DataChallengeMessageResp(attribute, value) =>
|
||||
attribute mustEqual "sample"
|
||||
value mustEqual 1L
|
||||
|
|
@ -21,7 +21,7 @@ class DataChallengeMessageRespTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DataChallengeMessageResp("sample", 1L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class DataChallengeMessageTest extends Specification {
|
|||
val string = hex"938673616d706c6501000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DataChallengeMessage(attribute, value) =>
|
||||
attribute mustEqual "sample"
|
||||
value mustEqual 1L
|
||||
|
|
@ -21,7 +21,7 @@ class DataChallengeMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DataChallengeMessage("sample", 1L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DelayedPathMountMsgTest extends Specification {
|
|||
val string = hex"5a f50583044680"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DelayedPathMountMsg(player_guid, vehicle_guid, u3, u4) =>
|
||||
player_guid mustEqual PlanetSideGUID(1525)
|
||||
vehicle_guid mustEqual PlanetSideGUID(1155)
|
||||
|
|
@ -24,7 +24,7 @@ class DelayedPathMountMsgTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DelayedPathMountMsg(PlanetSideGUID(1525), PlanetSideGUID(1155), 70, true)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class DensityLevelUpdateMessageTest extends Specification {
|
|||
val string = hex"cd 0100 1f4e 000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DensityLevelUpdateMessage(zone_id, building_id, unk) =>
|
||||
zone_id mustEqual 1
|
||||
building_id mustEqual 19999
|
||||
|
|
@ -30,23 +30,23 @@ class DensityLevelUpdateMessageTest extends Specification {
|
|||
|
||||
"encode" in {
|
||||
val msg = DensityLevelUpdateMessage(1, 19999, List(0, 0, 0, 0, 0, 0, 0, 0))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
||||
"encode (failure; wrong number of list entries)" in {
|
||||
val msg = DensityLevelUpdateMessage(1, 19999, List(0))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"encode (failure; list number too big)" in {
|
||||
val msg = DensityLevelUpdateMessage(1, 19999, List(0, 0, 0, 0, 0, 0, 0, 8))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"encode (failure; list number too small)" in {
|
||||
val msg = DensityLevelUpdateMessage(1, 19999, List(0, 0, 0, 0, 0, -1, 0, 0))
|
||||
PacketCoding.EncodePacket(msg).isSuccessful mustEqual false
|
||||
PacketCoding.encodePacket(msg).isSuccessful mustEqual false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DeployObjectMessageTest extends Specification {
|
|||
val string = hex"5d 740b e8030000 a644b 6e3c6 7e18 00 00 3f 01000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DeployObjectMessage(guid, unk1, pos, orient, unk2) =>
|
||||
guid mustEqual PlanetSideGUID(2932)
|
||||
unk1 mustEqual 1000L
|
||||
|
|
@ -31,7 +31,7 @@ class DeployObjectMessageTest extends Specification {
|
|||
Vector3.z(272.8125f),
|
||||
1L
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DeployRequestMessageTest extends Specification {
|
|||
val string = hex"4b 4b00 7c01 40 0cf73b52aa6a9300"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case DeployRequestMessage(player_guid, vehicle_guid, deploy_state, unk2, unk3, pos) =>
|
||||
player_guid mustEqual PlanetSideGUID(75)
|
||||
vehicle_guid mustEqual PlanetSideGUID(380)
|
||||
|
|
@ -35,7 +35,7 @@ class DeployRequestMessageTest extends Specification {
|
|||
false,
|
||||
Vector3(4060.1953f, 2218.8281f, 155.32812f)
|
||||
)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue