diff --git a/common/src/main/scala/net/psforever/config/ConfigParser.scala b/common/src/main/scala/net/psforever/config/ConfigParser.scala index 173a27eb9..24942d403 100644 --- a/common/src/main/scala/net/psforever/config/ConfigParser.scala +++ b/common/src/main/scala/net/psforever/config/ConfigParser.scala @@ -105,7 +105,17 @@ trait ConfigParser { protected val config_template : Seq[ConfigSection] // Misuse of this function can lead to run time exceptions when the types don't match - def Get[T : ConfigTypeRequired](key : String) : T = config_map(key).asInstanceOf[T] + // ClassTag is needed due to type erasure on T + // https://dzone.com/articles/scala-classtag-a-simple-use-case + def Get[T : ConfigTypeRequired](key : String)(implicit m: ClassTag[T]) : T = { + config_map.get(key) match { + case Some(value : T) => value + case None => + throw new NoSuchElementException(s"Config key '${key}' not found") + case Some(value : Any) => + throw new ClassCastException(s"Incorrect type T = ${m.runtimeClass.getSimpleName} passed to Get[T]: needed ${value.getClass.getSimpleName}") + } + } def Load(filename : String) : ValidationResult = { val ini = new org.ini4j.Ini() diff --git a/config/worldserver.ini.dist b/config/worldserver.ini.dist index a62c76033..1a240980d 100644 --- a/config/worldserver.ini.dist +++ b/config/worldserver.ini.dist @@ -48,6 +48,35 @@ ListeningPort = 51001 ListeningPort = 51000 +################################################################################################### +# NETWORK SETTINGS +################################################################################################### + +[network] + +# Session.InboundGraceTime (time) +# Description: The maximum amount of time since the last inbound packet from a UDP session +# before it is dropped. +# Important: Lower values will cause legitimate clients to be dropped during loading +# screens, but higher values will make the server be more susceptible to +# denial of service attacks and running out of memory. +# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace) +# Default: 1 minute - (Clients sending a packet at least +# once a minute stay alive) + +Session.InboundGraceTime = 1 minute + +# Session.OutboundGraceTime (time) +# Description: The maximum amount of time since the last outbound packet for a UDP session +# before it is dropped. Can be used as a watchdog for hung server sessions. +# Important: Lower values will cause legitimate clients to be dropped during server +# lag spikes or Zone transitions. +# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace) +# Default: 1 minute - (Clients receiving a packet at least +# once a minute stay alive) + +Session.OutboundGraceTime = 1 minute + ################################################################################################### # DEVELOPER SETTINGS # - NETWORK SIMULATOR diff --git a/pslogin/src/main/scala/SessionRouter.scala b/pslogin/src/main/scala/SessionRouter.scala index acc1d3c40..890512dd6 100644 --- a/pslogin/src/main/scala/SessionRouter.scala +++ b/pslogin/src/main/scala/SessionRouter.scala @@ -111,6 +111,9 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act log.error(s"Requested to drop non-existent session ID=$id from ${sender()}") } case SessionReaper() => + val inboundGrace = WorldConfig.Get[Duration]("network.Session.InboundGraceTime").toMillis + val outboundGrace = WorldConfig.Get[Duration]("network.Session.OutboundGraceTime").toMillis + sessionById.foreach { case (id, session) => log.trace(session.toString) if(session.getState == Closed()) { @@ -119,9 +122,9 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act sessionById.remove(id) idBySocket.remove(session.socketAddress) log.debug(s"Reaped session ID=$id") - } else if(session.timeSinceLastInboundEvent > 10000) { + } else if(session.timeSinceLastInboundEvent > inboundGrace) { removeSessionById(id, "session timed out (inbound)", graceful = false) - } else if(session.timeSinceLastOutboundEvent > 4000) { + } else if(session.timeSinceLastOutboundEvent > outboundGrace) { removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU } } diff --git a/pslogin/src/main/scala/WorldConfig.scala b/pslogin/src/main/scala/WorldConfig.scala index e6de5ae7d..baca9d02b 100644 --- a/pslogin/src/main/scala/WorldConfig.scala +++ b/pslogin/src/main/scala/WorldConfig.scala @@ -12,6 +12,10 @@ object WorldConfig extends ConfigParser { ConfigSection("worldserver", ConfigEntryInt("ListeningPort", 51001, Constraints.min(1), Constraints.max(65535)) ), + ConfigSection("network", + ConfigEntryTime("Session.InboundGraceTime", 1 minute, Constraints.min(10 seconds)), + ConfigEntryTime("Session.OutboundGraceTime", 1 minute, Constraints.min(10 seconds)) + ), ConfigSection("developer", ConfigEntryBool ("NetSim.Active", false), ConfigEntryFloat("NetSim.Loss", 0.02f, Constraints.min(0.0f), Constraints.max(1.0f)), diff --git a/pslogin/src/test/scala/ConfigTest.scala b/pslogin/src/test/scala/ConfigTest.scala index 56f9285cd..6bbea4336 100644 --- a/pslogin/src/test/scala/ConfigTest.scala +++ b/pslogin/src/test/scala/ConfigTest.scala @@ -1,4 +1,5 @@ // Copyright (c) 2019 PSForever +import java.io._ import scala.io.Source import org.specs2.mutable._ import net.psforever.config._ @@ -11,6 +12,25 @@ class ConfigTest extends Specification { "have no errors" in { WorldConfig.Load("config/worldserver.ini.dist") mustEqual Valid } + + "be formatted correctly" in { + var lineno = 1 + for (line <- Source.fromFile("config/worldserver.ini.dist").getLines) { + val linee :String = line + val ctx = s"worldserver.ini.dist:${lineno}" + val maxLen = 100 + val lineLen = line.length + + lineLen aka s"${ctx} - line length" must beLessThan(maxLen) + line.slice(0, 1) aka s"${ctx} - leading whitespace found" mustNotEqual " " + line.slice(line.length-1, line.length) aka s"${ctx} - trailing whitespace found" mustNotEqual " " + + lineno += 1 + } + + ok + } + } "TestConfig" should { @@ -26,6 +46,19 @@ class ConfigTest extends Specification { TestConfig.Get[Boolean]("default.bool_false") mustEqual false TestConfig.Get[Int]("default.missing") mustEqual 1337 } + + "throw when getting non-existant keys" in { + TestConfig.Load(testConfig) mustEqual Valid + TestConfig.Get[Int]("missing.key") must throwA[NoSuchElementException](message = "Config key 'missing.key' not found") + TestConfig.Get[String]("missing.key") must throwA[NoSuchElementException](message = "Config key 'missing.key' not found") + } + + "throw when Get is not passed the right type parameter" in { + TestConfig.Load(testConfig) mustEqual Valid + TestConfig.Get[Duration]("default.string") must throwA[ClassCastException](message = "Incorrect type T = Duration passed to Get\\[T\\]: needed String") + TestConfig.Get[String]("default.int") must throwA[ClassCastException](message = "Incorrect type T = String passed to Get\\[T\\]: needed Int") + ok + } } "TestBadConfig" should {