Made psoserv fully configurable and fixed a bug in the proxy server's encryption handling.

This commit is contained in:
Daan Vanden Bosch 2021-08-01 12:25:40 +02:00
parent 089832c2fe
commit d9f6869dd0
17 changed files with 465 additions and 238 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ build
.DS_Store
*.log
karma.config.generated.js
# Config
/psoserv/config.json

View File

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

47
psoserv/README.md Normal file
View File

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

View File

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

View File

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

View File

@ -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<String>) {
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<Server<*, *>>,
private val proxyServers: List<ProxyServer>,
) {
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<Server<*, *>>()
// 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<ProxyServer> {
if (!config.run) {
return emptyList()
}
val defaultBindAddress = config.bindAddress?.let(::inet4Address) ?: DEFAULT_ADDRESS
val defaultRemoteAddress = config.remoteAddress?.let(::inet4Address) ?: DEFAULT_ADDRESS
val redirectMap = mutableMapOf<Inet4Pair, Inet4Pair>()
val proxyServers = mutableListOf<ProxyServer>()
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<Message>
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
}

View File

@ -62,6 +62,42 @@ class GuildCard(
val entries: List<GuildCardEntry>
)
object BbMessageDescriptor : MessageDescriptor<BbMessage> {
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,

View File

@ -32,6 +32,14 @@ interface Message {
val bodySize: Int get() = size - headerSize
}
interface MessageDescriptor<out MessageType : Message> {
val headerSize: Int
fun readHeader(buffer: Buffer): Header
fun readMessage(buffer: Buffer): MessageType
}
interface InitEncryptionMessage : Message {
val serverKey: ByteArray
val clientKey: ByteArray

View File

@ -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<PcMessage> {
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,

View File

@ -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<StateType : ServerState<BbMessage, StateType>>(
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)
}

View File

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

View File

@ -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<Message>,
private val createCipher: (key: ByteArray) -> Cipher,
private val headerSize: Int,
private val readHeader: (Buffer) -> Header,
private val redirectMap: Map<Pair<Inet4Address, Int>, Pair<Inet4Address, Int>> = emptyMap(),
) : TrackedDisposable() {
private val proxySocket = ServerSocket(proxyPort, 50, proxyAddress)
private val redirectMap: Map<Inet4Pair, Inet4Pair> = 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<Message>(KotlinLogging.logger {}, serverSocket, headerSize) {
) : SocketHandler<Message>(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<Message>(KotlinLogging.logger {}, clientSocket, headerSize) {
) : SocketHandler<Message>(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 {}
}
}

View File

@ -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<MessageType : Message, StateType : ServerState<MessageType
private val logger: KLogger,
private val address: InetAddress,
private val port: Int,
) : TrackedDisposable() {
) {
private val serverSocket = ServerSocket(port, 50, address)
private var connectionCounter = 0
@Volatile
private var running = true
init {
logger.info { "Initializing." }
fun start() {
logger.info { "Starting." }
// Accept client connections on a dedicated thread.
val thread = Thread(::acceptConnections)
@ -33,7 +32,7 @@ abstract class Server<MessageType : Message, StateType : ServerState<MessageType
thread.start()
}
override fun dispose() {
fun stop() {
logger.info { "Stopping." }
// Signal to the connection thread that it should stop.
@ -42,8 +41,6 @@ abstract class Server<MessageType : Message, StateType : ServerState<MessageType
// Closing the server socket will generate a SocketException on the connection thread which
// will then shut down.
serverSocket.close()
super.dispose()
}
private fun acceptConnections() {

View File

@ -4,8 +4,8 @@ import mu.KLogger
import world.phantasmal.psolib.Endianness
import world.phantasmal.psolib.buffer.Buffer
import world.phantasmal.psoserv.encryption.Cipher
import world.phantasmal.psoserv.messages.Header
import world.phantasmal.psoserv.messages.Message
import world.phantasmal.psoserv.messages.MessageDescriptor
import world.phantasmal.psoserv.messages.messageString
import world.phantasmal.psoserv.roundToBlockSize
import java.net.Socket
@ -15,13 +15,14 @@ import kotlin.math.min
abstract class SocketHandler<MessageType : Message>(
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<MessageType>
protected abstract val decryptCipher: Cipher?
protected abstract val encryptCipher: Cipher?
@ -61,13 +62,13 @@ abstract class SocketHandler<MessageType : Message>(
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<MessageType : Message>(
bytesToSkip = encryptedSize - available
decryptCipher?.advance(
blocks = (encryptedSize - headerSize) / decryptCipher.blockSize,
)
encryptCipher?.advance(
blocks = encryptedSize / encryptCipher.blockSize,
)
@ -99,7 +104,7 @@ abstract class SocketHandler<MessageType : Message>(
)
try {
val message = readMessage(messageBuffer)
val message = messageDescriptor.readMessage(messageBuffer)
logger.trace { "Received $message." }
when (processMessage(message)) {
@ -192,7 +197,7 @@ abstract class SocketHandler<MessageType : Message>(
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<MessageType : Message>(
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) {

View File

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

View File

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

View File

@ -2,11 +2,11 @@
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} [%t] - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<Root level="trace">
<AppenderRef ref="Console"/>
</Root>
</Loggers>