diff --git a/.gitignore b/.gitignore index 68382cc4..d9e0f22d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ build .DS_Store *.log karma.config.generated.js + +# Config +/psoserv/config.json diff --git a/README.md b/README.md index 3bef846c..4415012a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ [Phantasmal World](https://www.phantasmal.world/) is a suite of tools for Phantasy Star Online. +## PSO Server + +Phantasmal world contains a [PSO server](psoserv/README.md). + ## Developers Phantasmal World is written in [Kotlin](https://kotlinlang.org/) and uses @@ -24,10 +28,11 @@ See [features](./FEATURES.md) for a list of features, planned features and bugs. 1. Install Java 11+ (e.g. [AdoptOpenJDK](https://adoptopenjdk.net/) or [GraalVM](https://www.graalvm.org/downloads/)) -2. `cd` to the project directory -3. Launch webpack server on [http://localhost:1623/](http://localhost:1623/) +2. Ensure the JAVA_HOME environment variable is set to JDK's location +3. `cd` to the project directory +4. Launch webpack server on [http://localhost:1623/](http://localhost:1623/) with `./gradlew :web:run --continuous` -4. [web/src/main/kotlin/world/phantasmal/web/Main.kt](web/src/main/kotlin/world/phantasmal/web/Main.kt) +5. [web/src/main/kotlin/world/phantasmal/web/Main.kt](web/src/main/kotlin/world/phantasmal/web/Main.kt) is the application's entry point [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) is recommended for development. IntelliJ @@ -50,9 +55,9 @@ The code base is divided up into the following gradle subprojects. Core contains the basic utilities that all other subprojects directly or indirectly depend on. -#### lib +#### psolib -Lib contains PSO file format parsers, compression/decompression code, a PSO script +Psolib contains PSO file format parsers, compression/decompression code, a PSO script assembler/disassembler and a work-in-progress script engine/VM. It also has a model of the PSO scripting bytecode and data flow analysis for it. This subproject can be used as a library in other projects. @@ -73,6 +78,10 @@ The actual Phantasmal World web application. Web GUI toolkit used by Phantasmal World. +#### [psoserv](psoserv/README.md) + +Work-in-progress PSO server and fully functional PSO proxy server. + ### Unit Tests Run the unit tests with `./gradlew check`. JS tests are run with Karma and Mocha, JVM tests with diff --git a/psoserv/README.md b/psoserv/README.md new file mode 100644 index 00000000..c1333a31 --- /dev/null +++ b/psoserv/README.md @@ -0,0 +1,47 @@ +# Phantasmal PSO Server + +## Configuration + +Put a config.json file in the directory where psoserv will run or pass +the `--config=/path/to/config.json` parameter to specify a configuration file. + +## Proxy + +Phantasmal PSO server can proxy any other PSO server. Below is a sample configuration for proxying a +locally running Tethealla server using a Tethealla client. Be sure to modify tethealla.ini and set +server port to 22000. + +```json +{ + "proxy": { + "bindAddress": "localhost", + "remoteAddress": "localhost", + "servers": [ + { + "name": "patch_proxy", + "version": "PC", + "bindPort": 11000, + "remotePort": 21000 + }, + { + "name": "patch_data_proxy", + "version": "PC", + "bindPort": 11001, + "remotePort": 21001 + }, + { + "name": "login_proxy", + "version": "BB", + "bindPort": 12000, + "remotePort": 22000 + }, + { + "name": "login_2_proxy", + "version": "BB", + "bindPort": 12001, + "remotePort": 22001 + } + ] + } +} +``` diff --git a/psoserv/build.gradle.kts b/psoserv/build.gradle.kts index 66bd8cbd..a50e9b1b 100644 --- a/psoserv/build.gradle.kts +++ b/psoserv/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("world.phantasmal.jvm") + kotlin("plugin.serialization") application } @@ -7,7 +8,10 @@ application { mainClass.set("world.phantasmal.psoserv.MainKt") } +val serializationVersion: String by project.extra + dependencies { implementation(project(":core")) implementation(project(":psolib")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt new file mode 100644 index 00000000..cdd67722 --- /dev/null +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Config.kt @@ -0,0 +1,50 @@ +package world.phantasmal.psoserv + +import kotlinx.serialization.Serializable + +@Serializable +class Config( + val address: String? = null, + val patch: PatchServerConfig? = null, + val login: ServerConfig? = null, + val data: ServerConfig? = null, + val proxy: ProxyConfig? = null, +) + +@Serializable +class ServerConfig( + val run: Boolean = true, + val address: String? = null, + val port: Int? = null, +) + +@Serializable +class PatchServerConfig( + val run: Boolean = true, + val welcomeMessage: String? = null, + val address: String? = null, + val port: Int? = null, +) + +@Serializable +class ProxyConfig( + val run: Boolean = true, + val bindAddress: String? = null, + val remoteAddress: String? = null, + val servers: List = emptyList(), +) + +@Serializable +class ProxyServerConfig( + val name: String? = null, + val version: GameVersionConfig, + val bindAddress: String? = null, + val bindPort: Int, + val remoteAddress: String? = null, + val remotePort: Int, +) + +@Serializable +enum class GameVersionConfig { + PC, BB +} diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt index 172497ca..576d79b1 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt @@ -1,115 +1,202 @@ package world.phantasmal.psoserv +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import mu.KotlinLogging import world.phantasmal.psoserv.encryption.BbCipher +import world.phantasmal.psoserv.encryption.Cipher import world.phantasmal.psoserv.encryption.PcCipher -import world.phantasmal.psoserv.messages.BB_HEADER_SIZE -import world.phantasmal.psoserv.messages.BbMessage -import world.phantasmal.psoserv.messages.PC_HEADER_SIZE -import world.phantasmal.psoserv.messages.PcMessage -import world.phantasmal.psoserv.servers.ProxyServer +import world.phantasmal.psoserv.messages.BbMessageDescriptor +import world.phantasmal.psoserv.messages.Message +import world.phantasmal.psoserv.messages.MessageDescriptor +import world.phantasmal.psoserv.messages.PcMessageDescriptor +import world.phantasmal.psoserv.servers.* import world.phantasmal.psoserv.servers.character.DataServer import world.phantasmal.psoserv.servers.login.LoginServer import world.phantasmal.psoserv.servers.patch.PatchServer +import java.io.File import java.net.Inet4Address -import java.net.InetAddress -private const val PATCH_SERVER_PORT: Int = 11_000 -private const val LOGIN_SERVER_PORT: Int = 12_000 -private const val DATA_SERVER_PORT: Int = 12_001 -private val LOGGER = KotlinLogging.logger {} +// System property java.net.preferIPv6Addresses should be false. +private val DEFAULT_ADDRESS: Inet4Address = inet4Loopback() +private const val DEFAULT_PATCH_PORT: Int = 11_000 +private const val DEFAULT_LOGIN_PORT: Int = 12_000 +private const val DEFAULT_DATA_PORT: Int = 12_001 -fun main() { +private val LOGGER = KotlinLogging.logger("main") + +fun main(args: Array) { LOGGER.info { "Initializing." } - if (true) { - // System property java.net.preferIPv6Addresses should be false. - val characterServerAddress = InetAddress.getLoopbackAddress() as? Inet4Address - ?: error("Couldn't get IPv4 address of character server.") + var configFile: File? = null - PatchServer( - InetAddress.getLoopbackAddress(), - port = PATCH_SERVER_PORT, - welcomeMessage = "Welcome to Phantasmal World.", - ) + for (arg in args) { + val split = arg.split('=') - LoginServer( - InetAddress.getLoopbackAddress(), - port = LOGIN_SERVER_PORT, - characterServerAddress, - DATA_SERVER_PORT, - ) + if (split.size == 2) { + val (param, value) = split - DataServer(InetAddress.getLoopbackAddress(), port = DATA_SERVER_PORT) - } else { - val loopback = InetAddress.getLoopbackAddress() as Inet4Address - val redirectMap = mapOf( - Pair(loopback, 21_001) to Pair(loopback, 11_001), - Pair(loopback, 22_001) to Pair(loopback, 12_001), - ) - ProxyServer( - proxyAddress = InetAddress.getLoopbackAddress(), - proxyPort = 11_000, - serverAddress = InetAddress.getLoopbackAddress(), - serverPort = 21_000, - PcMessage::fromBuffer, - ::PcCipher, - headerSize = PC_HEADER_SIZE, - PcMessage::readHeader, - redirectMap, - ) - ProxyServer( - proxyAddress = InetAddress.getLoopbackAddress(), - proxyPort = 11_001, - serverAddress = InetAddress.getLoopbackAddress(), - serverPort = 21_001, - PcMessage::fromBuffer, - ::PcCipher, - headerSize = PC_HEADER_SIZE, - PcMessage::readHeader, - redirectMap, - ) - ProxyServer( - proxyAddress = InetAddress.getLoopbackAddress(), - proxyPort = 12_000, - serverAddress = InetAddress.getLoopbackAddress(), - serverPort = 22_000, - BbMessage::fromBuffer, - ::BbCipher, - headerSize = BB_HEADER_SIZE, - BbMessage::readHeader, - redirectMap, - ) - ProxyServer( - proxyAddress = InetAddress.getLoopbackAddress(), - proxyPort = 12_001, - serverAddress = InetAddress.getLoopbackAddress(), - serverPort = 22_001, - BbMessage::fromBuffer, - ::BbCipher, - headerSize = BB_HEADER_SIZE, - BbMessage::readHeader, - redirectMap, - ) -// ProxyServer( -// proxyAddress = InetAddress.getLoopbackAddress(), -// proxyPort = 13_001, -// serverAddress = InetAddress.getByName("74.91.125.137"), -// serverPort = 13_001, -// ) -// ProxyServer( -// proxyAddress = InetAddress.getLoopbackAddress(), -// proxyPort = 14_000, -// serverAddress = InetAddress.getByName("74.91.125.137"), -// serverPort = 14_000, -// ) -// ProxyServer( -// proxyAddress = InetAddress.getLoopbackAddress(), -// proxyPort = 14_001, -// serverAddress = InetAddress.getByName("74.91.125.137"), -// serverPort = 14_001, -// ) + when (param) { + "--config" -> { + configFile = File(value) + } + } + } } - LOGGER.info { "Initialization finished." } + if (configFile == null) { + configFile = File("config.json").takeIf { it.isFile } + } + + val config: Config + + if (configFile != null) { + LOGGER.info { "Using configuration file $configFile." } + + val json = Json { + ignoreUnknownKeys = true + } + + config = json.decodeFromString(configFile.readText()) + } else { + config = Config() + } + + val server = initialize(config) + + LOGGER.info { "Starting up." } + + server.start() +} + +private class PhantasmalServer( + private val servers: List>, + private val proxyServers: List, +) { + fun start() { + servers.forEach(Server<*, *>::start) + proxyServers.forEach(ProxyServer::start) + } + + fun stop() { + servers.forEach(Server<*, *>::stop) + proxyServers.forEach(ProxyServer::stop) + } +} + +private fun initialize(config: Config): PhantasmalServer { + val defaultAddress = config.address?.let(::inet4Address) ?: DEFAULT_ADDRESS + + val dataAddress = config.data?.address?.let(::inet4Address) ?: defaultAddress + val dataPort = config.data?.port ?: DEFAULT_DATA_PORT + + val servers = mutableListOf>() + + // If no proxy config is specified, we run a regular PSO server by default. + val run = config.proxy == null || !config.proxy.run + + if (config.patch == null && run || config.patch?.run == true) { + val address = config.patch?.address?.let(::inet4Address) ?: defaultAddress + val port = config.patch?.port ?: DEFAULT_PATCH_PORT + + LOGGER.info { "Configuring patch server to bind to $address:$port." } + + servers.add( + PatchServer( + address, + port, + welcomeMessage = config.patch?.welcomeMessage ?: "Welcome to Phantasmal World.", + ) + ) + } + + if (config.login == null && run || config.login?.run == true) { + val address = config.login?.address?.let(::inet4Address) ?: defaultAddress + val port = config.login?.port ?: DEFAULT_LOGIN_PORT + + LOGGER.info { "Configuring login server to bind to $address:$port." } + + servers.add( + LoginServer( + address, + port, + dataServerAddress = dataAddress, + dataServerPort = dataPort, + ) + ) + } + + if (config.data == null && run || config.data?.run == true) { + val address = config.data?.address?.let(::inet4Address) ?: defaultAddress + val port = config.data?.port ?: DEFAULT_DATA_PORT + + LOGGER.info { "Configuring data server to bind to $address:$port." } + + servers.add( + DataServer( + address, + port, + ) + ) + } + + val proxyServers = config.proxy?.let(::initializeProxy) ?: emptyList() + + return PhantasmalServer(servers, proxyServers) +} + +private fun initializeProxy(config: ProxyConfig): List { + if (!config.run) { + return emptyList() + } + + val defaultBindAddress = config.bindAddress?.let(::inet4Address) ?: DEFAULT_ADDRESS + val defaultRemoteAddress = config.remoteAddress?.let(::inet4Address) ?: DEFAULT_ADDRESS + val redirectMap = mutableMapOf() + val proxyServers = mutableListOf() + var nameI = 1 + + for (psc in config.servers) { + val name = psc.name ?: "proxy_${nameI++}" + val bindPair = Inet4Pair( + psc.bindAddress?.let(::inet4Address) ?: defaultBindAddress, + psc.bindPort, + ) + val remotePair = Inet4Pair( + psc.remoteAddress?.let(::inet4Address) ?: defaultRemoteAddress, + psc.remotePort, + ) + redirectMap[remotePair] = bindPair + + val messageDescriptor: MessageDescriptor + val createCipher: (key: ByteArray) -> Cipher + + when (psc.version) { + GameVersionConfig.PC -> { + messageDescriptor = PcMessageDescriptor + createCipher = ::PcCipher + } + GameVersionConfig.BB -> { + messageDescriptor = BbMessageDescriptor + createCipher = ::BbCipher + } + } + + LOGGER.info { + """Configuring proxy server "$name" to bind to $bindPair and proxy remote server $remotePair.""" + } + + proxyServers.add( + ProxyServer( + name, + bindPair, + remotePair, + messageDescriptor, + createCipher, + redirectMap, + ) + ) + } + + return proxyServers } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt index b54f5b76..a309522b 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/BbMessages.kt @@ -62,6 +62,42 @@ class GuildCard( val entries: List ) +object BbMessageDescriptor : MessageDescriptor { + override val headerSize: Int = BB_HEADER_SIZE + + override fun readHeader(buffer: Buffer): Header { + val size = buffer.getUShort(BB_MSG_SIZE_POS).toInt() + val code = buffer.getUShort(BB_MSG_CODE_POS).toInt() + // Ignore 4 flag bytes. + return Header(code, size) + } + + override fun readMessage(buffer: Buffer): BbMessage = + when (buffer.getUShort(BB_MSG_CODE_POS).toInt()) { + // Sorted by low-order byte, then high-order byte. + 0x0003 -> BbMessage.InitEncryption(buffer) + 0x0005 -> BbMessage.Disconnect(buffer) + 0x0019 -> BbMessage.Redirect(buffer) + 0x0093 -> BbMessage.Authenticate(buffer) + 0x01DC -> BbMessage.GuildCardHeader(buffer) + 0x02DC -> BbMessage.GuildCardChunk(buffer) + 0x03DC -> BbMessage.GetGuildCardChunk(buffer) + 0x00E0 -> BbMessage.GetAccount(buffer) + 0x00E2 -> BbMessage.Account(buffer) + 0x00E3 -> BbMessage.CharacterSelect(buffer) + 0x00E5 -> BbMessage.CharacterSelectResponse(buffer) + 0x00E6 -> BbMessage.AuthenticationResponse(buffer) + 0x01E8 -> BbMessage.Checksum(buffer) + 0x02E8 -> BbMessage.ChecksumResponse(buffer) + 0x03E8 -> BbMessage.GetGuildCardHeader(buffer) + 0x01EB -> BbMessage.FileList(buffer) + 0x02EB -> BbMessage.FileChunk(buffer) + 0x03EB -> BbMessage.GetFileChunk(buffer) + 0x04EB -> BbMessage.GetFileList(buffer) + else -> BbMessage.Unknown(buffer) + } +} + sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_SIZE) { override val code: Int get() = buffer.getUShort(BB_MSG_CODE_POS).toInt() override val size: Int get() = buffer.getUShort(BB_MSG_SIZE_POS).toInt() @@ -302,38 +338,6 @@ sealed class BbMessage(override val buffer: Buffer) : AbstractMessage(BB_HEADER_ class Unknown(buffer: Buffer) : BbMessage(buffer) companion object { - fun readHeader(buffer: Buffer): Header { - val size = buffer.getUShort(BB_MSG_SIZE_POS).toInt() - val code = buffer.getUShort(BB_MSG_CODE_POS).toInt() - // Ignore 4 flag bytes. - return Header(code, size) - } - - fun fromBuffer(buffer: Buffer): BbMessage = - when (buffer.getUShort(BB_MSG_CODE_POS).toInt()) { - // Sorted by low-order byte, then high-order byte. - 0x0003 -> InitEncryption(buffer) - 0x0005 -> Disconnect(buffer) - 0x0019 -> Redirect(buffer) - 0x0093 -> Authenticate(buffer) - 0x01DC -> GuildCardHeader(buffer) - 0x02DC -> GuildCardChunk(buffer) - 0x03DC -> GetGuildCardChunk(buffer) - 0x00E0 -> GetAccount(buffer) - 0x00E2 -> Account(buffer) - 0x00E3 -> CharacterSelect(buffer) - 0x00E5 -> CharacterSelectResponse(buffer) - 0x00E6 -> AuthenticationResponse(buffer) - 0x01E8 -> Checksum(buffer) - 0x02E8 -> ChecksumResponse(buffer) - 0x03E8 -> GetGuildCardHeader(buffer) - 0x01EB -> FileList(buffer) - 0x02EB -> FileChunk(buffer) - 0x03EB -> GetFileChunk(buffer) - 0x04EB -> GetFileList(buffer) - else -> Unknown(buffer) - } - protected fun buf( code: Int, bodySize: Int = 0, diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt index 534a2456..6d9b1a24 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/Messages.kt @@ -32,6 +32,14 @@ interface Message { val bodySize: Int get() = size - headerSize } +interface MessageDescriptor { + val headerSize: Int + + fun readHeader(buffer: Buffer): Header + + fun readMessage(buffer: Buffer): MessageType +} + interface InitEncryptionMessage : Message { val serverKey: ByteArray val clientKey: ByteArray diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt index 23c660e6..b35d90ef 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/messages/PcMessages.kt @@ -13,6 +13,30 @@ const val PC_HEADER_SIZE: Int = 4 const val PC_MSG_SIZE_POS: Int = 0 const val PC_MSG_CODE_POS: Int = 2 +object PcMessageDescriptor : MessageDescriptor { + override val headerSize: Int = PC_HEADER_SIZE + + override fun readHeader(buffer: Buffer): Header { + val size = buffer.getUShort(PC_MSG_SIZE_POS).toInt() + val code = buffer.getUByte(PC_MSG_CODE_POS).toInt() + // Ignore flag byte at position 3. + return Header(code, size) + } + + override fun readMessage(buffer: Buffer): PcMessage = + when (buffer.getUByte(PC_MSG_CODE_POS).toInt()) { + 0x02 -> PcMessage.InitEncryption(buffer) + 0x04 -> PcMessage.Login(buffer) + 0x0B -> PcMessage.PatchListStart(buffer) + 0x0D -> PcMessage.PatchListEnd(buffer) + 0x10 -> PcMessage.PatchListOk(buffer) + 0x12 -> PcMessage.PatchDone(buffer) + 0x13 -> PcMessage.WelcomeMessage(buffer) + 0x14 -> PcMessage.Redirect(buffer) + else -> PcMessage.Unknown(buffer) + } +} + sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_SIZE) { override val code: Int get() = buffer.getUByte(PC_MSG_CODE_POS).toInt() override val size: Int get() = buffer.getUShort(PC_MSG_SIZE_POS).toInt() @@ -106,26 +130,6 @@ sealed class PcMessage(override val buffer: Buffer) : AbstractMessage(PC_HEADER_ class Unknown(buffer: Buffer) : PcMessage(buffer) companion object { - fun readHeader(buffer: Buffer, offset: Int = 0): Header { - val size = buffer.getUShort(offset + PC_MSG_SIZE_POS).toInt() - val code = buffer.getUByte(offset + PC_MSG_CODE_POS).toInt() - // Ignore flag byte at position 3. - return Header(code, size) - } - - fun fromBuffer(buffer: Buffer): PcMessage = - when (buffer.getUByte(PC_MSG_CODE_POS).toInt()) { - 0x02 -> InitEncryption(buffer) - 0x04 -> Login(buffer) - 0x0B -> PatchListStart(buffer) - 0x0D -> PatchListEnd(buffer) - 0x10 -> PatchListOk(buffer) - 0x12 -> PatchDone(buffer) - 0x13 -> WelcomeMessage(buffer) - 0x14 -> Redirect(buffer) - else -> Unknown(buffer) - } - protected fun buf( code: Int, bodySize: Int = 0, diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BbServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BbServer.kt index 1ba0b977..005a4887 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BbServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/BbServer.kt @@ -4,6 +4,7 @@ import mu.KLogger import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psoserv.encryption.BbCipher import world.phantasmal.psoserv.messages.BbMessage +import world.phantasmal.psoserv.messages.BbMessageDescriptor import world.phantasmal.psoserv.messages.Header import java.net.InetAddress @@ -16,8 +17,8 @@ abstract class BbServer>( override fun createCipher() = BbCipher() override fun readHeader(buffer: Buffer): Header = - BbMessage.readHeader(buffer) + BbMessageDescriptor.readHeader(buffer) override fun readMessage(buffer: Buffer): BbMessage = - BbMessage.fromBuffer(buffer) + BbMessageDescriptor.readMessage(buffer) } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Inet4.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Inet4.kt new file mode 100644 index 00000000..c048b791 --- /dev/null +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Inet4.kt @@ -0,0 +1,21 @@ +package world.phantasmal.psoserv.servers + +import java.net.Inet4Address +import java.net.InetAddress +import java.net.InetSocketAddress + +class Inet4Pair(addr: Inet4Address, port: Int) : InetSocketAddress(addr, port) { + constructor(addr: ByteArray, port: Int) : this(inet4Address(addr), port) + constructor(addr: String, port: Int) : this(inet4Address(addr), port) + + val address: Inet4Address get() = super.getAddress() as Inet4Address +} + +fun inet4Address(addr: ByteArray): Inet4Address = + InetAddress.getByAddress(addr) as Inet4Address + +fun inet4Address(addr: String): Inet4Address = + InetAddress.getByName(addr) as Inet4Address + +fun inet4Loopback(): Inet4Address = + InetAddress.getLoopbackAddress() as Inet4Address diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt index 9442a7a0..34fe0f1c 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/ProxyServer.kt @@ -1,40 +1,46 @@ package world.phantasmal.psoserv.servers import mu.KotlinLogging -import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psoserv.encryption.Cipher -import world.phantasmal.psoserv.messages.* -import java.net.* +import world.phantasmal.psoserv.messages.InitEncryptionMessage +import world.phantasmal.psoserv.messages.Message +import world.phantasmal.psoserv.messages.MessageDescriptor +import world.phantasmal.psoserv.messages.RedirectMessage +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.net.SocketTimeoutException class ProxyServer( - private val proxyAddress: InetAddress, - private val proxyPort: Int, - private val serverAddress: InetAddress, - private val serverPort: Int, - private val readMessage: (Buffer) -> Message, + private val name: String, + private val bindPair: Inet4Pair, + private val remotePair: Inet4Pair, + private val messageDescriptor: MessageDescriptor, private val createCipher: (key: ByteArray) -> Cipher, - private val headerSize: Int, - private val readHeader: (Buffer) -> Header, - private val redirectMap: Map, Pair> = emptyMap(), -) : TrackedDisposable() { - private val proxySocket = ServerSocket(proxyPort, 50, proxyAddress) + private val redirectMap: Map = emptyMap(), +) { + private val logger = KotlinLogging.logger(name) + private val proxySocket = ServerSocket() @Volatile - private var running = true - private var connected = false + private var running = false - init { - LOGGER.info { "Initializing." } + fun start() { + logger.info { "Starting." } + + proxySocket.bind(bindPair) + + running = true // Accept client connections on a dedicated thread. val thread = Thread(::acceptConnections) - thread.name = this::class.simpleName + thread.name = name thread.start() } - override fun dispose() { - LOGGER.info { "Stopping." } + fun stop() { + logger.info { "Stopping." } // Signal to the connection thread that it should stop. running = false @@ -42,28 +48,24 @@ class ProxyServer( // Closing the server socket will generate a SocketException on the connection thread which // will then shut down. proxySocket.close() - - super.dispose() } private fun acceptConnections() { if (running) { - LOGGER.info { "Accepting connections." } + logger.info { "Accepting connections." } while (running) { try { val clientSocket = proxySocket.accept() - LOGGER.info { + logger.info { "New client connection from ${clientSocket.inetAddress}:${clientSocket.port}." } - val serverSocket = Socket(serverAddress, serverPort) - LOGGER.info { + val serverSocket = Socket(remotePair.address, remotePair.port) + logger.info { "Connected to server ${serverSocket.inetAddress}:${serverSocket.port}." } - connected = true - // Listen to server on this thread. // Don't start listening to the client until encryption is initialized. ServerHandler(serverSocket, clientSocket).listen() @@ -71,49 +73,45 @@ class ProxyServer( // Retry after timeout. continue } catch (e: InterruptedException) { - LOGGER.error(e) { - "Interrupted while trying to accept client connections on $proxyAddress:$proxyPort, stopping." + logger.error(e) { + "Interrupted while trying to accept client connections on $bindPair, stopping." } break } catch (e: SocketException) { // Don't log if we're not running anymore because that means this exception was // probably generated by a socket.close() call. if (running) { - LOGGER.error(e) { - "Exception while trying to accept client connections on $proxyAddress:$proxyPort, stopping." + logger.error(e) { + "Exception while trying to accept client connections on $bindPair, stopping." } } break } catch (e: Throwable) { - LOGGER.error(e) { - "Exception while trying to accept client connections on $proxyAddress:$proxyPort." + logger.error(e) { + "Exception while trying to accept client connections on $bindPair." } } } } - LOGGER.info { "Stopped." } + logger.info { "Stopped." } } private inner class ServerHandler( serverSocket: Socket, private val clientSocket: Socket, - ) : SocketHandler(KotlinLogging.logger {}, serverSocket, headerSize) { + ) : SocketHandler(logger, serverSocket) { private var clientHandler: ClientHandler? = null + override val messageDescriptor = this@ProxyServer.messageDescriptor + // The first message sent by the server is always unencrypted and initializes the // encryption. We don't start listening to the client until the encryption is // initialized. override var decryptCipher: Cipher? = null override var encryptCipher: Cipher? = null - override fun readHeader(buffer: Buffer): Header = - this@ProxyServer.readHeader(buffer) - - override fun readMessage(buffer: Buffer): Message = - this@ProxyServer.readMessage(buffer) - override fun processMessage(message: Message): ProcessResult { when (message) { is InitEncryptionMessage -> if (decryptCipher == null) { @@ -136,20 +134,20 @@ class ProxyServer( ) this.clientHandler = clientListener val thread = Thread(clientListener::listen) - thread.name = "${ProxyServer::class.simpleName} client" + thread.name = "$name client" thread.start() } is RedirectMessage -> { - val oldAddress = InetAddress.getByAddress(message.ipAddress) + val oldAddress = Inet4Pair(message.ipAddress, message.port) - redirectMap[Pair(oldAddress, message.port)]?.let { (newAddress, newPort) -> + redirectMap[oldAddress]?.let { newAddress -> logger.debug { - "Rewriting redirect from $oldAddress:${message.port} to $newAddress:$newPort." + "Rewriting redirect from $oldAddress to $newAddress." } - message.ipAddress = newAddress.address - message.port = newPort + message.ipAddress = newAddress.address.address + message.port = newAddress.port return ProcessResult.Changed } @@ -174,13 +172,9 @@ class ProxyServer( private val serverHandler: ServerHandler, override val decryptCipher: Cipher, override val encryptCipher: Cipher, - ) : SocketHandler(KotlinLogging.logger {}, clientSocket, headerSize) { + ) : SocketHandler(logger, clientSocket) { - override fun readHeader(buffer: Buffer): Header = - this@ProxyServer.readHeader(buffer) - - override fun readMessage(buffer: Buffer): Message = - this@ProxyServer.readMessage(buffer) + override val messageDescriptor = this@ProxyServer.messageDescriptor override fun processMessage(message: Message): ProcessResult = ProcessResult.Ok @@ -192,8 +186,4 @@ class ProxyServer( serverHandler.stop() } } - - companion object { - private val LOGGER = KotlinLogging.logger {} - } } diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Server.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Server.kt index 1bbba335..e7647b55 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Server.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/Server.kt @@ -1,7 +1,6 @@ package world.phantasmal.psoserv.servers import mu.KLogger -import world.phantasmal.core.disposable.TrackedDisposable import world.phantasmal.psolib.Endianness import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psoserv.encryption.Cipher @@ -17,15 +16,15 @@ abstract class Server( protected val logger: KLogger, private val socket: Socket, - private val headerSize: Int, ) { - private val sockName: String = "${socket.inetAddress}:${socket.port}" + private val sockName: String = "${socket.remoteSocketAddress}" + private val headerSize: Int get() = messageDescriptor.headerSize @Volatile private var running = false + protected abstract val messageDescriptor: MessageDescriptor protected abstract val decryptCipher: Cipher? protected abstract val encryptCipher: Cipher? @@ -61,13 +62,13 @@ abstract class SocketHandler( decryptCipher.decrypt(headerBuffer) } - val (code, size) = readHeader(headerBuffer) + val (code, size) = messageDescriptor.readHeader(headerBuffer) val encryptedSize = roundToBlockSize(size, decryptCipher?.blockSize ?: 1) // Bytes available for the next message. val available = readBuffer.size - offset when { - // Don't parse message when it's too large. + // Don't parse the message when it's too large. encryptedSize > BUFFER_CAPACITY -> { logger.warn { val message = messageString(code, size) @@ -76,6 +77,10 @@ abstract class SocketHandler( bytesToSkip = encryptedSize - available + decryptCipher?.advance( + blocks = (encryptedSize - headerSize) / decryptCipher.blockSize, + ) + encryptCipher?.advance( blocks = encryptedSize / encryptCipher.blockSize, ) @@ -99,7 +104,7 @@ abstract class SocketHandler( ) try { - val message = readMessage(messageBuffer) + val message = messageDescriptor.readMessage(messageBuffer) logger.trace { "Received $message." } when (processMessage(message)) { @@ -192,7 +197,7 @@ abstract class SocketHandler( try { if (socket.isClosed) { - logger.info { "$sockName was closed." } + logger.info { "Connection to $sockName was closed." } } else { logger.info { "Closing connection to $sockName." } socket.close() @@ -212,10 +217,6 @@ abstract class SocketHandler( socket.close() } - protected abstract fun readHeader(buffer: Buffer): Header - - protected abstract fun readMessage(buffer: Buffer): MessageType - protected abstract fun processMessage(message: MessageType): ProcessResult protected open fun processRawBytes(buffer: Buffer, offset: Int, size: Int) { diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/login/LoginServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/login/LoginServer.kt index d9410e0f..07687a4a 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/login/LoginServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/login/LoginServer.kt @@ -9,12 +9,12 @@ import java.net.InetAddress class LoginServer( address: InetAddress, port: Int, - private val characterServerAddress: Inet4Address, - private val characterServerPort: Int, + private val dataServerAddress: Inet4Address, + private val dataServerPort: Int, ) : BbServer(KotlinLogging.logger {}, address, port) { override fun initializeState(sender: ClientSender): LoginState { - val ctx = LoginContext(sender, characterServerAddress.address, characterServerPort) + val ctx = LoginContext(sender, dataServerAddress.address, dataServerPort) ctx.send( BbMessage.InitEncryption( diff --git a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/patch/PatchServer.kt b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/patch/PatchServer.kt index fd68563c..52de6965 100644 --- a/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/patch/PatchServer.kt +++ b/psoserv/src/main/kotlin/world/phantasmal/psoserv/servers/patch/PatchServer.kt @@ -5,6 +5,7 @@ import world.phantasmal.psolib.buffer.Buffer import world.phantasmal.psoserv.encryption.PcCipher import world.phantasmal.psoserv.messages.Header import world.phantasmal.psoserv.messages.PcMessage +import world.phantasmal.psoserv.messages.PcMessageDescriptor import world.phantasmal.psoserv.servers.Server import java.net.InetAddress @@ -29,8 +30,8 @@ class PatchServer(address: InetAddress, port: Int, private val welcomeMessage: S } override fun readHeader(buffer: Buffer): Header = - PcMessage.readHeader(buffer) + PcMessageDescriptor.readHeader(buffer) override fun readMessage(buffer: Buffer): PcMessage = - PcMessage.fromBuffer(buffer) + PcMessageDescriptor.readMessage(buffer) } diff --git a/psoserv/src/main/resources/log4j2.xml b/psoserv/src/main/resources/log4j2.xml index c52aaeaf..597b608c 100644 --- a/psoserv/src/main/resources/log4j2.xml +++ b/psoserv/src/main/resources/log4j2.xml @@ -2,11 +2,11 @@ - + - +