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:
Jakob Gillich 2020-09-17 17:04:06 +02:00
parent 5827204b10
commit 407429ee21
232 changed files with 2906 additions and 4385 deletions

View file

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

View file

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

@ -12,6 +12,7 @@ out/
.metals
project/metals.sbt
/docs
.vscode
# User configs
config/psforever.conf

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
sbt.version = 1.3.8
sbt.version = 1.3.13

View file

@ -0,0 +1 @@
stacktrace.app.packages=net.psforever

View file

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

View file

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

View file

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

View file

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

View 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))
}
}
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()
}

View 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)
}
}

View file

@ -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 _ => ()
}
}

View file

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

View file

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

View 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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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