mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-12 06:55:10 +00:00
Create advance session management and reaping
This commit is contained in:
parent
c1257cb1ec
commit
ae6687c38f
10 changed files with 301 additions and 84 deletions
|
|
@ -15,7 +15,8 @@ lazy val commonSettings = Seq(
|
||||||
"ch.qos.logback" % "logback-classic" % "1.1.7",
|
"ch.qos.logback" % "logback-classic" % "1.1.7",
|
||||||
"org.log4s" %% "log4s" % "1.3.0",
|
"org.log4s" %% "log4s" % "1.3.0",
|
||||||
"org.fusesource.jansi" % "jansi" % "1.12",
|
"org.fusesource.jansi" % "jansi" % "1.12",
|
||||||
"org.scoverage" %% "scalac-scoverage-plugin" % "1.1.1"
|
"org.scoverage" %% "scalac-scoverage-plugin" % "1.1.1",
|
||||||
|
"com.github.nscala-time" %% "nscala-time" % "2.12.0"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ object ControlPacketOpcode extends Enumeration {
|
||||||
ServerStart, // second packet sent in response to ClientStart
|
ServerStart, // second packet sent in response to ClientStart
|
||||||
MultiPacket, // used to send multiple packets with one UDP message (subpackets limited to <= 255)
|
MultiPacket, // used to send multiple packets with one UDP message (subpackets limited to <= 255)
|
||||||
Unknown4,
|
Unknown4,
|
||||||
Unknown5,
|
TeardownConnection,
|
||||||
Unknown6,
|
Unknown6,
|
||||||
ControlSync, // sent to the server from the client
|
ControlSync, // sent to the server from the client
|
||||||
ControlSyncResp, // the response generated by the server
|
ControlSyncResp, // the response generated by the server
|
||||||
|
|
@ -61,7 +61,7 @@ object ControlPacketOpcode extends Enumeration {
|
||||||
case ClientStart => control.ClientStart.decode
|
case ClientStart => control.ClientStart.decode
|
||||||
case MultiPacket => control.MultiPacket.decode
|
case MultiPacket => control.MultiPacket.decode
|
||||||
case Unknown4 => noDecoder(opcode)
|
case Unknown4 => noDecoder(opcode)
|
||||||
case Unknown5 => noDecoder(opcode)
|
case TeardownConnection => control.TeardownConnection.decode
|
||||||
case Unknown6 => noDecoder(opcode)
|
case Unknown6 => noDecoder(opcode)
|
||||||
case ControlSync => control.ControlSync.decode
|
case ControlSync => control.ControlSync.decode
|
||||||
case ControlSyncResp => control.ControlSyncResp.decode
|
case ControlSyncResp => control.ControlSyncResp.decode
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
|
package net.psforever.packet.control
|
||||||
|
|
||||||
|
import net.psforever.packet.{ControlPacketOpcode, Marshallable, PacketHelpers, PlanetSideControlPacket}
|
||||||
|
import scodec.Codec
|
||||||
|
import scodec.codecs._
|
||||||
|
|
||||||
|
final case class TeardownConnection(targetNonce : Long) extends PlanetSideControlPacket {
|
||||||
|
type Packet = TeardownConnection
|
||||||
|
def opcode = ControlPacketOpcode.TeardownConnection
|
||||||
|
def encode = TeardownConnection.encode(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
object TeardownConnection extends Marshallable[TeardownConnection] {
|
||||||
|
implicit val codec : Codec[TeardownConnection] = (
|
||||||
|
("nonce" | uint32L) ::
|
||||||
|
("unk" | uint16).unit(6)
|
||||||
|
).as[TeardownConnection]
|
||||||
|
}
|
||||||
|
|
@ -167,5 +167,24 @@ class ControlPacketTest extends Specification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"TeardownConnection" should {
|
||||||
|
val string = hex"00 05 02 4F 57 17 00 06"
|
||||||
|
|
||||||
|
"decode" in {
|
||||||
|
PacketCoding.DecodePacket(string).require match {
|
||||||
|
case TeardownConnection(nonce) =>
|
||||||
|
nonce mustEqual 391597826
|
||||||
|
case default =>
|
||||||
|
ko
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"encode" in {
|
||||||
|
val encoded = PacketCoding.EncodePacket(TeardownConnection(391597826)).require
|
||||||
|
|
||||||
|
encoded.toByteVector mustEqual string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,12 +11,15 @@ import scodec.{Attempt, Codec, Err}
|
||||||
import scodec.codecs.{bytes, uint16L, uint8L}
|
import scodec.codecs.{bytes, uint16L, uint8L}
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
import net.psforever.packet.control.{ClientStart, ServerStart}
|
import net.psforever.packet.control.{ClientStart, ServerStart, TeardownConnection}
|
||||||
import net.psforever.packet.crypto._
|
import net.psforever.packet.crypto._
|
||||||
import net.psforever.packet.game.PingMsg
|
import net.psforever.packet.game.PingMsg
|
||||||
import org.log4s.MDC
|
import org.log4s.MDC
|
||||||
import MDCContextAware.Implicits._
|
import MDCContextAware.Implicits._
|
||||||
|
|
||||||
|
sealed trait CryptoSessionAPI
|
||||||
|
final case class DropCryptoSession() extends CryptoSessionAPI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actor that stores crypto state for a connection, appropriately encrypts and decrypts packets,
|
* Actor that stores crypto state for a connection, appropriately encrypts and decrypts packets,
|
||||||
* and passes packets along to the next hop once processed.
|
* and passes packets along to the next hop once processed.
|
||||||
|
|
@ -41,6 +44,9 @@ class CryptoSessionActor extends Actor with MDCContextAware {
|
||||||
var clientChallenge = ByteVector.empty
|
var clientChallenge = ByteVector.empty
|
||||||
var clientChallengeResult = ByteVector.empty
|
var clientChallengeResult = ByteVector.empty
|
||||||
|
|
||||||
|
var clientNonce : Long = 0
|
||||||
|
var serverNonce : Long = 0
|
||||||
|
|
||||||
// Don't leak crypto object memory even on an exception
|
// Don't leak crypto object memory even on an exception
|
||||||
override def postStop() = {
|
override def postStop() = {
|
||||||
cleanupCrypto()
|
cleanupCrypto()
|
||||||
|
|
@ -74,7 +80,9 @@ class CryptoSessionActor extends Actor with MDCContextAware {
|
||||||
|
|
||||||
p match {
|
p match {
|
||||||
case ControlPacket(_, ClientStart(nonce)) =>
|
case ControlPacket(_, ClientStart(nonce)) =>
|
||||||
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
|
clientNonce = nonce
|
||||||
|
serverNonce = Math.abs(random.nextInt())
|
||||||
|
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, serverNonce)))
|
||||||
|
|
||||||
context.become(CryptoExchange)
|
context.become(CryptoExchange)
|
||||||
case default =>
|
case default =>
|
||||||
|
|
@ -252,6 +260,14 @@ class CryptoSessionActor extends Actor with MDCContextAware {
|
||||||
case Failure(e) => log.error("Could not decode raw packet: " + e)
|
case Failure(e) => log.error("Could not decode raw packet: " + e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case api : CryptoSessionAPI =>
|
||||||
|
api match {
|
||||||
|
case DropCryptoSession() =>
|
||||||
|
handleEstablishedPacket(
|
||||||
|
sender(),
|
||||||
|
PacketCoding.CreateControlPacket(TeardownConnection(clientNonce))
|
||||||
|
)
|
||||||
|
}
|
||||||
case ctrl @ ControlPacket(_, _) =>
|
case ctrl @ ControlPacket(_, _) =>
|
||||||
val from = sender()
|
val from = sender()
|
||||||
|
|
||||||
|
|
@ -260,6 +276,8 @@ class CryptoSessionActor extends Actor with MDCContextAware {
|
||||||
val from = sender()
|
val from = sender()
|
||||||
|
|
||||||
handleEstablishedPacket(from, game)
|
handleEstablishedPacket(from, game)
|
||||||
|
case sessionAPI : SessionRouterAPI =>
|
||||||
|
leftRef !> sessionAPI
|
||||||
case default => failWithError(s"Invalid message '$default' received in state Established")
|
case default => failWithError(s"Invalid message '$default' received in state Established")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2016 PSForever.net to present
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
import java.net.{InetAddress, InetSocketAddress}
|
import java.net.{InetAddress, InetSocketAddress}
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorRef, MDCContextAware}
|
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||||
import net.psforever.packet.{PlanetSideGamePacket, _}
|
import net.psforever.packet.{PlanetSideGamePacket, _}
|
||||||
import net.psforever.packet.control._
|
import net.psforever.packet.control._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
|
|
@ -10,17 +10,26 @@ import scodec.Attempt.{Failure, Successful}
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
import MDCContextAware.Implicits._
|
import MDCContextAware.Implicits._
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
class LoginSessionActor extends Actor with MDCContextAware {
|
class LoginSessionActor extends Actor with MDCContextAware {
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
private case class UpdateServerList()
|
private case class UpdateServerList()
|
||||||
|
|
||||||
var sessionId : Long = 0
|
var sessionId : Long = 0
|
||||||
var leftRef : ActorRef = ActorRef.noSender
|
var leftRef : ActorRef = ActorRef.noSender
|
||||||
var rightRef : ActorRef = ActorRef.noSender
|
var rightRef : ActorRef = ActorRef.noSender
|
||||||
|
|
||||||
|
var updateServerListTask : Cancellable = null
|
||||||
|
|
||||||
|
override def postStop() = {
|
||||||
|
if(updateServerListTask != null)
|
||||||
|
updateServerListTask.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
def receive = Initializing
|
def receive = Initializing
|
||||||
|
|
||||||
def Initializing : Receive = {
|
def Initializing : Receive = {
|
||||||
|
|
@ -112,12 +121,14 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
||||||
0, 1, 2, 685276011, username, 0, false)
|
0, 1, 2, 685276011, username, 0, false)
|
||||||
|
|
||||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||||
updateServerList()
|
|
||||||
|
updateServerListTask = context.system.scheduler.schedule(0 seconds, 2 seconds, self, UpdateServerList())
|
||||||
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
|
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
|
||||||
log.info(s"Connect to world request for '${name}'")
|
log.info(s"Connect to world request for '${name}'")
|
||||||
|
|
||||||
val response = ConnectToWorldMessage(serverName, serverAddress.getHostString, serverAddress.getPort)
|
val response = ConnectToWorldMessage(serverName, serverAddress.getHostString, serverAddress.getPort)
|
||||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||||
|
sendResponse(DropSession(sessionId, "user transferring to world"))
|
||||||
case default => log.debug(s"Unhandled GamePacket ${pkt}")
|
case default => log.debug(s"Unhandled GamePacket ${pkt}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +147,7 @@ class LoginSessionActor extends Actor with MDCContextAware {
|
||||||
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
||||||
}
|
}
|
||||||
|
|
||||||
def sendResponse(cont : PlanetSidePacketContainer) = {
|
def sendResponse(cont : Any) = {
|
||||||
log.trace("LOGIN SEND: " + cont)
|
log.trace("LOGIN SEND: " + cont)
|
||||||
|
|
||||||
MDC("sessionId") = sessionId.toString
|
MDC("sessionId") = sessionId.toString
|
||||||
|
|
|
||||||
|
|
@ -174,9 +174,9 @@ object PsLogin {
|
||||||
val worldServerPort = 51001
|
val worldServerPort = 51001
|
||||||
|
|
||||||
/** Create two actors for handling the login and world server endpoints */
|
/** Create two actors for handling the login and world server endpoints */
|
||||||
val listener = system.actorOf(Props(new UdpListener(Props(new SessionRouter(loginTemplate)), "login-session-router",
|
val listener = system.actorOf(Props(new UdpListener(Props(new SessionRouter("Login", loginTemplate)), "login-session-router",
|
||||||
LoginConfig.serverIpAddress, loginServerPort)), "login-udp-endpoint")
|
LoginConfig.serverIpAddress, loginServerPort)), "login-udp-endpoint")
|
||||||
val worldListener = system.actorOf(Props(new UdpListener(Props(new SessionRouter(worldTemplate)), "world-session-router",
|
val worldListener = system.actorOf(Props(new UdpListener(Props(new SessionRouter("World", worldTemplate)), "world-session-router",
|
||||||
LoginConfig.serverIpAddress, worldServerPort)), "world-udp-endpoint")
|
LoginConfig.serverIpAddress, worldServerPort)), "world-udp-endpoint")
|
||||||
|
|
||||||
logger.info(s"NOTE: Set client.ini to point to ${LoginConfig.serverIpAddress.getHostAddress}:$loginServerPort")
|
logger.info(s"NOTE: Set client.ini to point to ${LoginConfig.serverIpAddress.getHostAddress}:$loginServerPort")
|
||||||
|
|
|
||||||
101
pslogin/src/main/scala/Session.scala
Normal file
101
pslogin/src/main/scala/Session.scala
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import org.log4s.MDC
|
||||||
|
import scodec.bits._
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, ActorRef, PoisonPill}
|
||||||
|
import com.github.nscala_time.time.Imports._
|
||||||
|
import MDCContextAware.Implicits._
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.head ! HelloFriend(sessionId, pipeline.tail.head)
|
||||||
|
|
||||||
|
// 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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,23 +9,43 @@ import scala.collection.mutable
|
||||||
import MDCContextAware.Implicits._
|
import MDCContextAware.Implicits._
|
||||||
import akka.actor.MDCContextAware.MdcMsg
|
import akka.actor.MDCContextAware.MdcMsg
|
||||||
import akka.actor.SupervisorStrategy.Stop
|
import akka.actor.SupervisorStrategy.Stop
|
||||||
|
import net.psforever.packet.PacketCoding
|
||||||
|
import net.psforever.packet.control.ConnectionClose
|
||||||
|
|
||||||
final case class RawPacket(data : ByteVector)
|
import scala.concurrent.duration._
|
||||||
final case class ResponsePacket(data : ByteVector)
|
|
||||||
|
|
||||||
case class SessionState(id : Long, address : InetSocketAddress, pipeline : List[ActorRef]) {
|
sealed trait SessionRouterAPI
|
||||||
def startOfPipe = pipeline.head
|
final case class RawPacket(data : ByteVector) extends SessionRouterAPI
|
||||||
def nextOfStart = pipeline.tail.head
|
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)
|
case class SessionPipeline(nameTemplate : String, props : Props)
|
||||||
|
|
||||||
class SessionRouter(pipeline : List[SessionPipeline]) extends Actor with MDCContextAware {
|
/**
|
||||||
|
* 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)
|
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
val sessionReaper = context.system.scheduler.schedule(10 seconds, 5 seconds, self, SessionReaper())
|
||||||
|
|
||||||
val idBySocket = mutable.Map[InetSocketAddress, Long]()
|
val idBySocket = mutable.Map[InetSocketAddress, Long]()
|
||||||
val sessionById = mutable.Map[Long, SessionState]()
|
val sessionById = mutable.Map[Long, Session]()
|
||||||
val sessionByActor = mutable.Map[ActorRef, SessionState]()
|
val sessionByActor = mutable.Map[ActorRef, Session]()
|
||||||
|
val closePacket = PacketCoding.EncodePacket(ConnectionClose()).require.bytes
|
||||||
|
|
||||||
var sessionId = 0L // this is a connection session, not an actual logged in session ID
|
var sessionId = 0L // this is a connection session, not an actual logged in session ID
|
||||||
var inputRef : ActorRef = ActorRef.noSender
|
var inputRef : ActorRef = ActorRef.noSender
|
||||||
|
|
@ -33,24 +53,9 @@ class SessionRouter(pipeline : List[SessionPipeline]) extends Actor with MDCCont
|
||||||
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
||||||
|
|
||||||
override def preStart = {
|
override def preStart = {
|
||||||
log.info("SessionRouter started...ready for PlanetSide sessions")
|
log.info(s"SessionRouter started...ready for ${role} sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 |
|
|
||||||
* +--------------+ +-----------+ +-----------------+
|
|
||||||
**/
|
|
||||||
|
|
||||||
def receive = initializing
|
def receive = initializing
|
||||||
|
|
||||||
def initializing : Receive = {
|
def initializing : Receive = {
|
||||||
|
|
@ -62,47 +67,70 @@ class SessionRouter(pipeline : List[SessionPipeline]) extends Actor with MDCCont
|
||||||
context.stop(self)
|
context.stop(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def postStop() = {
|
||||||
|
sessionReaper.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
def started : Receive = {
|
def started : Receive = {
|
||||||
case ReceivedPacket(msg, from) =>
|
case recv @ ReceivedPacket(msg, from) =>
|
||||||
if(idBySocket.contains(from)) {
|
var session : Session = null
|
||||||
MDC("sessionId") = idBySocket{from}.toString
|
|
||||||
|
|
||||||
log.trace(s"Handling received packet ${msg} -> ${sessionById{idBySocket{from}}.startOfPipe.path.name}")
|
if(!idBySocket.contains(from)) {
|
||||||
sessionById{idBySocket{from}}.startOfPipe !> RawPacket(msg)
|
session = createNewSession(from)
|
||||||
|
}
|
||||||
MDC.clear()
|
else {
|
||||||
} else {
|
val id = idBySocket{from}
|
||||||
val session = createNewSession(from)
|
session = sessionById{id}
|
||||||
idBySocket{from} = session.id
|
}
|
||||||
|
|
||||||
sessionById{session.id} = session
|
|
||||||
sessionByActor{session.startOfPipe} = session
|
|
||||||
sessionByActor{session.nextOfStart} = session
|
|
||||||
|
|
||||||
MDC("sessionId") = session.id.toString
|
|
||||||
|
|
||||||
log.info("New session from " + from.toString)
|
|
||||||
|
|
||||||
// send the initial message with MDC context (give the session ID to the lower layers)
|
|
||||||
sessionById{session.id}.startOfPipe !> HelloFriend(session.id, sessionById{session.id}.nextOfStart)
|
|
||||||
sessionById{session.id}.startOfPipe !> RawPacket(msg)
|
|
||||||
|
|
||||||
|
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()
|
MDC.clear()
|
||||||
}
|
}
|
||||||
case ResponsePacket(msg) =>
|
case ResponsePacket(msg) =>
|
||||||
val session = sessionByActor.get(sender())
|
val session = sessionByActor.get(sender())
|
||||||
if(session.isDefined) {
|
|
||||||
log.trace(s"Sending response ${msg}")
|
|
||||||
|
|
||||||
inputRef ! SendPacket(msg, session.get.address)
|
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 {
|
} else {
|
||||||
log.error("Dropped old response packet from session actor " + sender().path.name)
|
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() =>
|
||||||
|
sessionById.foreach { case (id, session) =>
|
||||||
|
log.debug(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 > 10000) {
|
||||||
|
removeSessionById(id, "session timed out (inbound)", graceful = false)
|
||||||
|
} else if(session.timeSinceLastOutboundEvent > 4000) {
|
||||||
|
removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case Terminated(actor) =>
|
case Terminated(actor) =>
|
||||||
val terminatedSession = sessionByActor.get(actor)
|
val terminatedSession = sessionByActor.get(actor)
|
||||||
|
|
||||||
if(terminatedSession.isDefined) {
|
if(terminatedSession.isDefined) {
|
||||||
removeSessionById(terminatedSession.get.id, s"${actor.path.name} died")
|
removeSessionById(terminatedSession.get.sessionId, s"${actor.path.name} died",
|
||||||
|
graceful = true)
|
||||||
} else {
|
} else {
|
||||||
log.error("Received an invalid actor Termination from " + actor.path.name)
|
log.error("Received an invalid actor Termination from " + actor.path.name)
|
||||||
}
|
}
|
||||||
|
|
@ -112,33 +140,38 @@ class SessionRouter(pipeline : List[SessionPipeline]) extends Actor with MDCCont
|
||||||
|
|
||||||
def createNewSession(address : InetSocketAddress) = {
|
def createNewSession(address : InetSocketAddress) = {
|
||||||
val id = newSessionId
|
val id = newSessionId
|
||||||
|
val session = new Session(id, address, inputRef, pipeline)
|
||||||
|
|
||||||
// inflate the pipeline
|
// establish mappings for easy lookup
|
||||||
val actors = pipeline.map { actor =>
|
idBySocket{address} = id
|
||||||
val a = context.actorOf(actor.props, actor.nameTemplate + id.toString)
|
sessionById{id} = session
|
||||||
context.watch(a)
|
|
||||||
a
|
session.getPipeline.foreach { actor =>
|
||||||
|
sessionByActor{actor} = session
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionState(id, address, actors)
|
log.info(s"New session ID=${id} from " + address.toString)
|
||||||
|
|
||||||
|
session
|
||||||
}
|
}
|
||||||
|
|
||||||
def removeSessionById(id : Long, reason : String) : Unit = {
|
def removeSessionById(id : Long, reason : String, graceful : Boolean) : Unit = {
|
||||||
val sessionOption = sessionById.get(id)
|
val sessionOption = sessionById.get(id)
|
||||||
|
|
||||||
if(sessionOption.isEmpty)
|
if(sessionOption.isEmpty)
|
||||||
return
|
return
|
||||||
|
|
||||||
val session = sessionOption.get
|
val session : Session = sessionOption.get
|
||||||
|
|
||||||
|
if(graceful) {
|
||||||
|
for(i <- 0 to 5) {
|
||||||
|
session.send(closePacket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: add some sort of delay to prevent old session packets from coming through
|
|
||||||
// kill all session specific actors
|
// kill all session specific actors
|
||||||
session.pipeline.foreach(_ ! PoisonPill)
|
session.dropSession(graceful)
|
||||||
session.pipeline.foreach(sessionByActor remove)
|
log.info(s"Dropping session ID=${id} (reason: $reason)")
|
||||||
sessionById.remove(id)
|
|
||||||
idBySocket.remove(session.address)
|
|
||||||
|
|
||||||
log.info(s"Stopping session ${id} (reason: $reason)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def newSessionId = {
|
def newSessionId = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2016 PSForever.net to present
|
// Copyright (c) 2016 PSForever.net to present
|
||||||
import java.net.{InetAddress, InetSocketAddress}
|
import java.net.{InetAddress, InetSocketAddress}
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorRef, MDCContextAware}
|
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||||
import net.psforever.packet.{PlanetSideGamePacket, _}
|
import net.psforever.packet.{PlanetSideGamePacket, _}
|
||||||
import net.psforever.packet.control._
|
import net.psforever.packet.control._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
|
|
@ -9,6 +9,7 @@ import scodec.Attempt.{Failure, Successful}
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
import org.log4s.MDC
|
import org.log4s.MDC
|
||||||
import MDCContextAware.Implicits._
|
import MDCContextAware.Implicits._
|
||||||
|
import net.psforever.types.ChatMessageType
|
||||||
|
|
||||||
class WorldSessionActor extends Actor with MDCContextAware {
|
class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
|
|
@ -19,6 +20,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
var leftRef : ActorRef = ActorRef.noSender
|
var leftRef : ActorRef = ActorRef.noSender
|
||||||
var rightRef : ActorRef = ActorRef.noSender
|
var rightRef : ActorRef = ActorRef.noSender
|
||||||
|
|
||||||
|
var clientKeepAlive : Cancellable = null
|
||||||
|
|
||||||
|
override def postStop() = {
|
||||||
|
if(clientKeepAlive != null)
|
||||||
|
clientKeepAlive.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
def receive = Initializing
|
def receive = Initializing
|
||||||
|
|
||||||
def Initializing : Receive = {
|
def Initializing : Receive = {
|
||||||
|
|
@ -139,7 +147,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
context.system.scheduler.schedule(0 seconds, 1000 milliseconds, self, PokeClient())
|
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 1000 milliseconds, self, PokeClient())
|
||||||
}
|
}
|
||||||
case default =>
|
case default =>
|
||||||
log.error("Unsupported " + default + " in " + msg)
|
log.error("Unsupported " + default + " in " + msg)
|
||||||
|
|
@ -160,6 +168,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
|
case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
|
||||||
log.info("Chat: " + msg)
|
log.info("Chat: " + msg)
|
||||||
|
|
||||||
|
// TODO: handle this appropriately
|
||||||
|
if(messagetype == ChatMessageType.PopupQuit) {
|
||||||
|
sendResponse(DropCryptoSession())
|
||||||
|
sendResponse(DropSession(sessionId, "user quit"))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing
|
// TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing
|
||||||
// TODO: Just replays the packet straight back to sender; actually needs to be routed to recipients!
|
// TODO: Just replays the packet straight back to sender; actually needs to be routed to recipients!
|
||||||
sendResponse(PacketCoding.CreateGamePacket(0, ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents)))
|
sendResponse(PacketCoding.CreateGamePacket(0, ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents)))
|
||||||
|
|
@ -235,17 +249,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
||||||
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
||||||
}
|
}
|
||||||
|
|
||||||
def sendResponse(cont : PlanetSidePacketContainer) = {
|
def sendResponse(cont : PlanetSidePacketContainer) : Unit = {
|
||||||
log.trace("WORLD SEND: " + cont)
|
log.trace("WORLD SEND: " + cont)
|
||||||
|
sendResponse(cont.asInstanceOf[Any])
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendResponse(msg : Any) : Unit = {
|
||||||
MDC("sessionId") = sessionId.toString
|
MDC("sessionId") = sessionId.toString
|
||||||
rightRef !> cont
|
rightRef !> msg
|
||||||
}
|
}
|
||||||
|
|
||||||
def sendRawResponse(pkt : ByteVector) = {
|
def sendRawResponse(pkt : ByteVector) = {
|
||||||
log.trace("WORLD SEND RAW: " + pkt)
|
log.trace("WORLD SEND RAW: " + pkt)
|
||||||
|
sendResponse(RawPacket(pkt))
|
||||||
MDC("sessionId") = sessionId.toString
|
|
||||||
rightRef !> RawPacket(pkt)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue