mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Started psoserv, a PSO server and PSO proxy server.
This commit is contained in:
parent
1d87b32986
commit
6daddcfd65
13
psoserv/build.gradle.kts
Normal file
13
psoserv/build.gradle.kts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
plugins {
|
||||||
|
id("world.phantasmal.jvm")
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("world.phantasmal.psoserv.MainKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":core"))
|
||||||
|
implementation(project(":psolib"))
|
||||||
|
}
|
115
psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt
Normal file
115
psoserv/src/main/kotlin/world/phantasmal/psoserv/Main.kt
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package world.phantasmal.psoserv
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import world.phantasmal.psoserv.encryption.BbCipher
|
||||||
|
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.servers.character.DataServer
|
||||||
|
import world.phantasmal.psoserv.servers.login.LoginServer
|
||||||
|
import world.phantasmal.psoserv.servers.patch.PatchServer
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
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.")
|
||||||
|
|
||||||
|
PatchServer(
|
||||||
|
InetAddress.getLoopbackAddress(),
|
||||||
|
port = PATCH_SERVER_PORT,
|
||||||
|
welcomeMessage = "Welcome to Phantasmal World.",
|
||||||
|
)
|
||||||
|
|
||||||
|
LoginServer(
|
||||||
|
InetAddress.getLoopbackAddress(),
|
||||||
|
port = LOGIN_SERVER_PORT,
|
||||||
|
characterServerAddress,
|
||||||
|
DATA_SERVER_PORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info { "Initialization finished." }
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package world.phantasmal.psoserv
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds [n] up so that it's divisible by [blockSize].
|
||||||
|
*/
|
||||||
|
fun roundToBlockSize(n: Int, blockSize: Int): Int =
|
||||||
|
n + (blockSize - n % blockSize) % blockSize
|
@ -0,0 +1,448 @@
|
|||||||
|
package world.phantasmal.psoserv.encryption
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import kotlin.experimental.xor
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blowfish with some modifications.
|
||||||
|
*/
|
||||||
|
class BbCipher(override val key: ByteArray = createKey()) : Cipher {
|
||||||
|
private val pArray: UIntArray = P_ARRAY.copyOf()
|
||||||
|
private val sBoxes: Array<UIntArray> = Array(S_BOXES.size) { S_BOXES[it].copyOf() }
|
||||||
|
private val scrambledKey: ByteArray = ByteArray(KEY_SIZE)
|
||||||
|
|
||||||
|
override val blockSize: Int = 8
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(key.size == KEY_SIZE) {
|
||||||
|
"Expected key of size $KEY_SIZE, but was ${key.size}."
|
||||||
|
}
|
||||||
|
|
||||||
|
scrambleKey()
|
||||||
|
initPArrayAndSBoxes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrambleKey() {
|
||||||
|
for (i in key.indices step 3) {
|
||||||
|
scrambledKey[i] = key[i] xor 0x19
|
||||||
|
scrambledKey[i + 1] = key[i + 1] xor 0x16
|
||||||
|
scrambledKey[i + 2] = key[i + 2] xor 0x18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPArrayAndSBoxes() {
|
||||||
|
// PSOBB P-array scramble.
|
||||||
|
for (i in pArray.indices) {
|
||||||
|
val pt = ((pArray[i] and 0xFF00u) shr 8) or ((pArray[i] and 0x00FFu) shl 8)
|
||||||
|
pArray[i] = (((pArray[i] shr 16) xor pt) shl 16) or pt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Blowfish P-array initialization.
|
||||||
|
for (i in pArray.indices) {
|
||||||
|
var k = 0u
|
||||||
|
val keyPos = 4 * (i % 12)
|
||||||
|
|
||||||
|
repeat(4) {
|
||||||
|
k = (k shl 8) or scrambledKey[keyPos + it].toUByte().toUInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pArray[i] = pArray[i] xor k
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Blowfish key expansion.
|
||||||
|
var l = 0u
|
||||||
|
var r = 0u
|
||||||
|
|
||||||
|
for (i in pArray.indices step 2) {
|
||||||
|
encryptBlock(l, r, setL = { l = it }, setR = { r = it }, iterations = 16)
|
||||||
|
pArray[i] = l
|
||||||
|
pArray[i + 1] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
for (sBox in sBoxes) {
|
||||||
|
for (i in sBox.indices step 2) {
|
||||||
|
encryptBlock(l, r, setL = { l = it }, setR = { r = it }, iterations = 16)
|
||||||
|
sBox[i] = l
|
||||||
|
sBox[i + 1] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encrypt(data: Buffer, offset: Int, blocks: Int) {
|
||||||
|
require(offset >= 0)
|
||||||
|
require(blocks >= 0)
|
||||||
|
val limit = offset + blocks * blockSize
|
||||||
|
require(limit <= data.size)
|
||||||
|
|
||||||
|
for (i in offset until limit step blockSize) {
|
||||||
|
val lIndex = i
|
||||||
|
val rIndex = i + 4
|
||||||
|
encryptBlock(
|
||||||
|
l = data.getUInt(lIndex),
|
||||||
|
r = data.getUInt(rIndex),
|
||||||
|
setL = { data.setUInt(lIndex, it) },
|
||||||
|
setR = { data.setUInt(rIndex, it) },
|
||||||
|
iterations = 4,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun encryptBlock(
|
||||||
|
l: UInt,
|
||||||
|
r: UInt,
|
||||||
|
setL: (UInt) -> Unit,
|
||||||
|
setR: (UInt) -> Unit,
|
||||||
|
iterations: Int,
|
||||||
|
) {
|
||||||
|
var newL = l
|
||||||
|
var newR = r
|
||||||
|
|
||||||
|
for (i in 0 until iterations) {
|
||||||
|
newL = newL xor pArray[i]
|
||||||
|
newR = newR xor f(newL)
|
||||||
|
// Swap l and r.
|
||||||
|
val tmp = newL
|
||||||
|
newL = newR
|
||||||
|
newR = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
newL = newL xor pArray[iterations]
|
||||||
|
newR = newR xor pArray[iterations + 1]
|
||||||
|
|
||||||
|
// Swap l and r.
|
||||||
|
setL(newR)
|
||||||
|
setR(newL)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decrypt(data: Buffer, offset: Int, blocks: Int) {
|
||||||
|
require(offset >= 0)
|
||||||
|
require(blocks >= 0)
|
||||||
|
val limit = offset + blocks * blockSize
|
||||||
|
require(limit <= data.size)
|
||||||
|
|
||||||
|
for (i in offset until limit step blockSize) {
|
||||||
|
decryptBlock(data, lIndex = i, rIndex = i + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptBlock(data: Buffer, lIndex: Int, rIndex: Int) {
|
||||||
|
var l = data.getUInt(lIndex)
|
||||||
|
var r = data.getUInt(rIndex)
|
||||||
|
|
||||||
|
for (i in 5 downTo 2) {
|
||||||
|
l = l xor pArray[i]
|
||||||
|
r = r xor f(l)
|
||||||
|
// Swap l and r.
|
||||||
|
val tmp = l
|
||||||
|
l = r
|
||||||
|
r = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l xor pArray[1]
|
||||||
|
r = r xor pArray[0]
|
||||||
|
|
||||||
|
// Swap l and r.
|
||||||
|
data.setUInt(lIndex, r)
|
||||||
|
data.setUInt(rIndex, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun advance(blocks: Int) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun f(x: UInt): UInt {
|
||||||
|
var h = sBoxes[0][(x shr 24).toInt()]
|
||||||
|
h += sBoxes[1][((x shr 16) and 0xFFu).toInt()]
|
||||||
|
h = h xor sBoxes[2][((x shr 8) and 0xFFu).toInt()]
|
||||||
|
h += sBoxes[3][(x and 0xFFu).toInt()]
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_SIZE = 48
|
||||||
|
|
||||||
|
fun createKey(): ByteArray = Random.nextBytes(KEY_SIZE)
|
||||||
|
|
||||||
|
private val P_ARRAY: UIntArray = uintArrayOf(
|
||||||
|
0x640cded2u, 0xca6cf7cfu, 0xc7bc95fbu, 0x7d0d60a3u, 0xcf23ad88u, 0x8ffb62dcu,
|
||||||
|
0x6c3da5ccu, 0x6bfcd6d6u, 0x63f492dfu, 0xe32ebe65u, 0xc3746b6du, 0xc5703934u,
|
||||||
|
0xdc940bceu, 0x590e0892u, 0xea9413e8u, 0xf4b13de7u, 0x505893fcu, 0xe3d696e3u,
|
||||||
|
)
|
||||||
|
private val S_BOXES: Array<UIntArray> = arrayOf(
|
||||||
|
uintArrayOf(
|
||||||
|
0x5CC73FD6u, 0x19572A8Eu, 0x1EAD320Eu, 0x29913B33u,
|
||||||
|
0x05C06104u, 0xC5A1316Eu, 0x456D82A7u, 0x5A987789u,
|
||||||
|
0xBFDCAA97u, 0x23094413u, 0x70B100F7u, 0xEF18F524u,
|
||||||
|
0x9B2632B1u, 0x1A7AA450u, 0x36355519u, 0x1A8FC2ADu,
|
||||||
|
0xE13D6A17u, 0xC74AF6AFu, 0xB771FC73u, 0x8C332A8Cu,
|
||||||
|
0x13792C10u, 0xA707616Fu, 0x69D18CE4u, 0x4BB744C2u,
|
||||||
|
0x74DA584Bu, 0xC2186564u, 0xBEF96BFDu, 0x9FF42F9Eu,
|
||||||
|
0xF6334290u, 0x74249103u, 0xC0D5CCCCu, 0xACC295F2u,
|
||||||
|
0x7BB4D473u, 0xBA4753A2u, 0x46806E9Fu, 0x7F8EB321u,
|
||||||
|
0x16803FE6u, 0x891FD2BCu, 0xE7218373u, 0x82CDF207u,
|
||||||
|
0x819879E3u, 0xB0CDE742u, 0xA9160843u, 0x14336F73u,
|
||||||
|
0xFC0B51A2u, 0x2FD13817u, 0x231C4134u, 0xBEA851C9u,
|
||||||
|
0x1B8B5DBFu, 0xB225A875u, 0x6BCC7EC4u, 0xCC5C66EFu,
|
||||||
|
0xB5C06E89u, 0xDC479976u, 0x1B984ED0u, 0x35BD70C1u,
|
||||||
|
0x8EC73F26u, 0xC85ED3FBu, 0x93A2CFF3u, 0xC0C1889Fu,
|
||||||
|
0x74C405A6u, 0xB4BA3842u, 0x62A89A52u, 0x850373D1u,
|
||||||
|
0xA8AD015Eu, 0x4087946Au, 0x1E81C985u, 0xE0278FEEu,
|
||||||
|
0x6D38EC05u, 0xBF4E158Au, 0x63E32BDDu, 0x17578163u,
|
||||||
|
0x9861C874u, 0x535ED4CFu, 0xE0674A4Au, 0xA2233B6Cu,
|
||||||
|
0x523574E3u, 0x35D19568u, 0x0247AFF9u, 0xED2BF2A5u,
|
||||||
|
0x1A404CC6u, 0x5700A52Cu, 0x3F5847FCu, 0x9139F9FCu,
|
||||||
|
0x8721985Au, 0x17A0493Bu, 0xF333A0D4u, 0x489411FFu,
|
||||||
|
0xD92EF4DBu, 0x1E50C960u, 0x833757F6u, 0xBBC00C1Bu,
|
||||||
|
0x01558F29u, 0x68058035u, 0xDB7D2645u, 0x5D11A667u,
|
||||||
|
0xAEE02660u, 0x3F5B5474u, 0xE3CF9E79u, 0x8E0E6574u,
|
||||||
|
0x2E4CEC6Fu, 0xB900EBB0u, 0x30F703C1u, 0xE73ECEE4u,
|
||||||
|
0x907D69CBu, 0xB785648Du, 0xEE57BBE1u, 0xA0862CB0u,
|
||||||
|
0xE942E1C5u, 0x2C7D0221u, 0xDF5445F7u, 0xD8C8AC9Fu,
|
||||||
|
0x22F05641u, 0x3E295EACu, 0x1E138FAAu, 0x3598F64Du,
|
||||||
|
0xDA199769u, 0xF157C46Du, 0x7CA171C5u, 0x94301DB9u,
|
||||||
|
0xFDC90D52u, 0x387128A3u, 0x41D7C806u, 0xD3190DABu,
|
||||||
|
0x3ACD7A85u, 0xE83EBBE3u, 0x14322C57u, 0x26845B42u,
|
||||||
|
0xB2CD49CBu, 0xE4D22B24u, 0x23C11989u, 0xE4FCD996u,
|
||||||
|
0x0FC3AD3Du, 0xE17A680Cu, 0xF4F0F8D8u, 0x72350D14u,
|
||||||
|
0x4C747633u, 0xC9633B10u, 0xFEC3618Bu, 0xFDE8DD1Cu,
|
||||||
|
0x9369EDF4u, 0xC8AECED7u, 0xE7160549u, 0x75BD584Cu,
|
||||||
|
0xF0451846u, 0xCEFB421Cu, 0x50FC8705u, 0xD67643AEu,
|
||||||
|
0x970AFDE8u, 0x09F8DEBAu, 0x6E82EAADu, 0x80CEB947u,
|
||||||
|
0x51AFE307u, 0x727B3F2Fu, 0xB22B287Bu, 0xF077F03Au,
|
||||||
|
0x4B670178u, 0x1F942DDEu, 0x37AFEAFFu, 0xE569CDE3u,
|
||||||
|
0xB78DD11Du, 0x6E8307D1u, 0x95CE57C6u, 0xC0E34476u,
|
||||||
|
0x2CA562D1u, 0x6373D161u, 0x2E549898u, 0xC6F47EC6u,
|
||||||
|
0x4A2A6BE4u, 0x6898DD70u, 0xFF954A7Cu, 0x8F033CD0u,
|
||||||
|
0xCD64C8E8u, 0x3C0A7D7Bu, 0xA3057D95u, 0xECD438E0u,
|
||||||
|
0xC111363Au, 0xB94FD214u, 0x7F224DFEu, 0xF042A491u,
|
||||||
|
0x9F1489FCu, 0x75E73DC9u, 0x1EA04F71u, 0xA38F2685u,
|
||||||
|
0x8BA7AF61u, 0x8DBF33DFu, 0x4EACD05Du, 0x3CEF9B0Eu,
|
||||||
|
0x9604FE9Fu, 0xB65D9990u, 0xBBCB14BAu, 0x06FC3A41u,
|
||||||
|
0xE15376DEu, 0x97D9BC59u, 0x8318618Au, 0x2DB10C0Cu,
|
||||||
|
0x3736FC1Fu, 0x6E8136D8u, 0x7E470DB5u, 0xC60DAED2u,
|
||||||
|
0x5A19532Fu, 0x98094AA8u, 0xE830FEA2u, 0x126A0685u,
|
||||||
|
0x2B76B98Fu, 0xA378F291u, 0xD36FB474u, 0xA3849120u,
|
||||||
|
0x7868242Au, 0x87743EA2u, 0x1D74914Fu, 0xB341998Cu,
|
||||||
|
0xD5B45B60u, 0xCD97DD2Eu, 0x9CEF94C3u, 0x907D0C7Bu,
|
||||||
|
0xAA967285u, 0x2C0C2B35u, 0x852D480Bu, 0xAFB7455Cu,
|
||||||
|
0x0FB40A91u, 0xDE019AC6u, 0xF285AA86u, 0xB5AF214Bu,
|
||||||
|
0x94E3A9D8u, 0x61CC82DEu, 0x592CE330u, 0x24943EEFu,
|
||||||
|
0xC689113Fu, 0x68A7FE73u, 0xCE85CAE0u, 0x9477D5B7u,
|
||||||
|
0x7EE161B8u, 0x1C4F6B1Bu, 0xAD1073F1u, 0xFBA9FFF8u,
|
||||||
|
0x11A5CE22u, 0x19BE7AF3u, 0x8646D47Au, 0xDD92E45Bu,
|
||||||
|
0xA5B089C8u, 0x05DB18A7u, 0xD915FB67u, 0xAE545E52u,
|
||||||
|
0x738B8333u, 0xE351E074u, 0xD846F324u, 0x4C4C85AEu,
|
||||||
|
0x1F705EAFu, 0x3C65970Cu, 0xB540A652u, 0x08355576u,
|
||||||
|
0x88FD52F2u, 0x1176FA93u, 0x04D2406Au, 0xA53E17C7u,
|
||||||
|
),
|
||||||
|
uintArrayOf(
|
||||||
|
0xC5FB6441u, 0xD36FC212u, 0x5C5AC0C9u, 0xE2C932C2u,
|
||||||
|
0xD22A7467u, 0xAD1D4B06u, 0xDC30354Au, 0x09F640EAu,
|
||||||
|
0x1B063309u, 0x0777B7A2u, 0xE30F2845u, 0xB16ED5E6u,
|
||||||
|
0x897B6ABFu, 0x1E2EC223u, 0xCFB0AC5Cu, 0x0297F232u,
|
||||||
|
0x7F56F89Du, 0xA3F50491u, 0x7C847191u, 0x61D4B903u,
|
||||||
|
0x25EE2690u, 0x58F77A26u, 0xC2D527FEu, 0x8123AFBEu,
|
||||||
|
0x7DFF42E6u, 0x9572104Bu, 0x15D8E9F6u, 0x23F908C8u,
|
||||||
|
0x1156A4DCu, 0xF8816E83u, 0xAEA972ECu, 0x9095ECFBu,
|
||||||
|
0xFDD7AFADu, 0xAA156F86u, 0x3306C3ADu, 0x5B21343Du,
|
||||||
|
0x13D0F0D9u, 0xA9098ABFu, 0x522944F1u, 0x76D2A256u,
|
||||||
|
0xE259A0B5u, 0x4675D80Du, 0x8B3DFC79u, 0xB9A76F83u,
|
||||||
|
0xF168CD53u, 0x0609A55Bu, 0x98E96452u, 0xB17832D9u,
|
||||||
|
0x8A90CBC9u, 0xC0229573u, 0x17266917u, 0x20055F24u,
|
||||||
|
0xAAF79B0Du, 0xE0D393EBu, 0x282C0B07u, 0x63AF3BBEu,
|
||||||
|
0x9FD9AE8Fu, 0xA0325E5Cu, 0x759B22ACu, 0xABB02882u,
|
||||||
|
0xAA56E55Cu, 0xA302AA9Eu, 0x95E40019u, 0x1F41E3C9u,
|
||||||
|
0x164B605Du, 0x30CD6081u, 0xF46F6677u, 0x66FDDBB7u,
|
||||||
|
0xAE738ACEu, 0x64A9B3FFu, 0x76CB795Fu, 0x8671B0E4u,
|
||||||
|
0x946FDF07u, 0x0F0712DCu, 0x14BE281Au, 0xEE01E411u,
|
||||||
|
0x5473C49Fu, 0xDD572435u, 0x6183D89Bu, 0xD8946913u,
|
||||||
|
0xCCA66FF9u, 0x39D5A9BEu, 0x3B1A7D18u, 0xA72B5D96u,
|
||||||
|
0x111E8E30u, 0xDAB26740u, 0x3F64B3DEu, 0xD1695E1Au,
|
||||||
|
0x33A19648u, 0x31DC630Au, 0xF5F35694u, 0xB91ED674u,
|
||||||
|
0x06FE9043u, 0xBE9E4E5Bu, 0xDA426AABu, 0x535055ECu,
|
||||||
|
0x0D2B265Eu, 0xEF43B103u, 0xB7EDF4B1u, 0xAA2618F5u,
|
||||||
|
0xA3D00018u, 0xEFA242CCu, 0x49D47F55u, 0x562677C2u,
|
||||||
|
0x7D41EEDAu, 0x40BF3AA5u, 0x135B8EEAu, 0x5DBED1DAu,
|
||||||
|
0x99BC688Au, 0xFE073B61u, 0x34E62A8Eu, 0x5125D336u,
|
||||||
|
0xDC70A9A6u, 0x292C52B4u, 0x2C7E2F60u, 0x04647F1Fu,
|
||||||
|
0x8A1989C4u, 0xEBA69244u, 0xA54A3897u, 0xFAC0D4D0u,
|
||||||
|
0xAD47205Bu, 0xF794C013u, 0xCD3C0A23u, 0xBA9671ACu,
|
||||||
|
0x8D1EAEA6u, 0x0DE2E83Eu, 0x9FBEE730u, 0xFA0684A3u,
|
||||||
|
0x42D96104u, 0x0E97CE42u, 0x698374A6u, 0xEF7D8288u,
|
||||||
|
0xF590DE72u, 0x6899F987u, 0x1BFD58ECu, 0x38B4B274u,
|
||||||
|
0x088A50AEu, 0xAE2113B0u, 0xE64CF295u, 0xBB67F9BEu,
|
||||||
|
0xDFA77BC0u, 0x598481EAu, 0x13E267B8u, 0xA7EB1033u,
|
||||||
|
0x7CA6DDDAu, 0x4A836CEDu, 0xBF89C618u, 0xBFBE9DAEu,
|
||||||
|
0xA44FE33Au, 0xA0BE3198u, 0xED12AF84u, 0x20976BE3u,
|
||||||
|
0x4754AA5Bu, 0x72930C88u, 0xB68D8550u, 0x532558E9u,
|
||||||
|
0x230F5F40u, 0xC0BD9035u, 0x672F3482u, 0xA89A61BFu,
|
||||||
|
0x4AA288DCu, 0x2045C67Au, 0xC59B9AE6u, 0xA0337DF9u,
|
||||||
|
0xE1857270u, 0xFD3DFF5Du, 0xE301EC12u, 0x50FFAE66u,
|
||||||
|
0x89DFE89Cu, 0x768C6E14u, 0x0AA10D87u, 0xFE2FEEF1u,
|
||||||
|
0x61B3A2EEu, 0xD5A31E6Fu, 0x7789B9F2u, 0x0FF5C3B1u,
|
||||||
|
0x29F1C194u, 0x77D011B0u, 0xECC10B84u, 0x6F931750u,
|
||||||
|
0x70B62A8Fu, 0xBB83CEFEu, 0x5F497EB2u, 0xF17666D6u,
|
||||||
|
0x5D785704u, 0x865C980Bu, 0xF0249EC2u, 0xAAE844DBu,
|
||||||
|
0x4CD28E52u, 0xDA93ADE9u, 0xD966908Cu, 0xA4B9FDDCu,
|
||||||
|
0x1FAE7671u, 0x96513D07u, 0x98F07CB6u, 0x7C13B222u,
|
||||||
|
0x1F05FFE7u, 0xFF903B48u, 0xC8D0DBBBu, 0xA6E52EB5u,
|
||||||
|
0x7D7BC10Au, 0xAFE0D2F7u, 0x01B79CC8u, 0x578225F9u,
|
||||||
|
0xE40C41B3u, 0xB5C7E26Au, 0xA46286EFu, 0x7B138D12u,
|
||||||
|
0x432661B3u, 0xC9C8124Eu, 0xE4BE379Bu, 0x34AEE10Du,
|
||||||
|
0x59AFF4CBu, 0xDAD26C27u, 0x5C9561B8u, 0x4D6B0452u,
|
||||||
|
0x10955F82u, 0x8AAD8718u, 0x4AAF2843u, 0xB94C51F7u,
|
||||||
|
0x756FF181u, 0xE701F22Eu, 0xA70427EEu, 0x52654509u,
|
||||||
|
0x2E4C3CABu, 0x33E7AF57u, 0xCCDC8F42u, 0xB8B3CA13u,
|
||||||
|
0x9122C3F3u, 0xF074441Au, 0x48E1C890u, 0xAE102653u,
|
||||||
|
0xC977A7F2u, 0x2FE76749u, 0x754513C2u, 0xA2A86DF9u,
|
||||||
|
0x7312F6B7u, 0xCCA4E105u, 0xACFB96CDu, 0xA0A9A9B2u,
|
||||||
|
0x237FAF6Du, 0x45B7EB4Du, 0x0C3E5872u, 0x460C5991u,
|
||||||
|
0x97248330u, 0xA47541B2u, 0xBF76D53Bu, 0x6C6C782Bu,
|
||||||
|
0x38A76A50u, 0x712E9FECu, 0xE7071507u, 0x0E4202B2u,
|
||||||
|
0x95A4154Eu, 0x62F6DA87u, 0x3DFD5418u, 0xD7AB33F5u,
|
||||||
|
),
|
||||||
|
uintArrayOf(
|
||||||
|
0x8E13062Cu, 0x2CEEE22Eu, 0x0B54E6A6u, 0xD073C03Au,
|
||||||
|
0x3D3F670Eu, 0xDB090F3Au, 0xCB73AB2Du, 0x210CC211u,
|
||||||
|
0x79FC9477u, 0x56DB66CEu, 0x7607573Au, 0xC56D0340u,
|
||||||
|
0x0D6F50E7u, 0x0F911F2Au, 0x16F5699Bu, 0x63123CB0u,
|
||||||
|
0x0015F81Bu, 0xFC22CC2Bu, 0x6594C4BAu, 0x1D645134u,
|
||||||
|
0x8633C3C5u, 0x6565D5D9u, 0xC902200Bu, 0x8EA7AA6Eu,
|
||||||
|
0xA28B3D86u, 0x9F22EF15u, 0x9E80E834u, 0x1931D611u,
|
||||||
|
0xD25095EDu, 0xDCE57608u, 0xBE54D17Au, 0xB75B7B77u,
|
||||||
|
0xFF53C715u, 0x6D1FE6F3u, 0xF4F1E1E8u, 0x507749B1u,
|
||||||
|
0x0C153DB4u, 0x7E80AD1Cu, 0xA5791026u, 0xAD3DBE27u,
|
||||||
|
0x7A65A28Fu, 0x9361771Bu, 0x570CC089u, 0x8D3412AAu,
|
||||||
|
0xA68FD2E0u, 0xDAB72770u, 0x2A303EDCu, 0x6477E936u,
|
||||||
|
0x16F913E0u, 0x09274ED9u, 0xE49A321Du, 0x1E64052Eu,
|
||||||
|
0x74AB96C9u, 0xD5FDD822u, 0x3DB27BD0u, 0x13E13918u,
|
||||||
|
0xD083F603u, 0xA4CC1CD1u, 0x2FF33194u, 0x8F610AB0u,
|
||||||
|
0xA1472C0Fu, 0x618F44D7u, 0x25294EABu, 0x4D6915BFu,
|
||||||
|
0xFCE933D0u, 0x32454A0Au, 0xA0BDC3A7u, 0xA5E7417Cu,
|
||||||
|
0x736BE207u, 0xE1859393u, 0x4B2BA3CAu, 0x689C8713u,
|
||||||
|
0xA1431A31u, 0xB1E88845u, 0xF1AB868Bu, 0x5A832C62u,
|
||||||
|
0xB774E1EAu, 0xF334763Cu, 0x1692AA49u, 0xDEBB4312u,
|
||||||
|
0x934B30B3u, 0x551E3EEDu, 0x7E832F92u, 0x73E7DF4Au,
|
||||||
|
0x0E51B5EBu, 0xEFA0C479u, 0x08804ADFu, 0x770EE5F0u,
|
||||||
|
0x3F35314Au, 0x9E2CABCCu, 0x40C2F1E4u, 0xE9764A79u,
|
||||||
|
0xE947E751u, 0x52261A4Du, 0x8C0A9EE8u, 0x23E5D212u,
|
||||||
|
0x954E09E5u, 0xCD1AF9F0u, 0x23B48F97u, 0x5A1A7DDCu,
|
||||||
|
0xC4D467CFu, 0x8A1301D3u, 0x30A40AE0u, 0xDC9B40A1u,
|
||||||
|
0x102BFB9Fu, 0x5A429B7Fu, 0xB0025E38u, 0x58D3215Eu,
|
||||||
|
0xCD199BDBu, 0x6738E9BDu, 0xD063B1F4u, 0xF72FFC51u,
|
||||||
|
0x56C10096u, 0xA7959937u, 0xA9E12B93u, 0x40C42AB1u,
|
||||||
|
0xA812D5CAu, 0x712A414Eu, 0x55242B16u, 0x3C1E0AD7u,
|
||||||
|
0x069B7F70u, 0xF7B3E6C8u, 0x5A592AA1u, 0x84438CA2u,
|
||||||
|
0xBC775FD6u, 0xA9B80BD7u, 0x089BAD81u, 0x0D8DE9CCu,
|
||||||
|
0xC8B58CC9u, 0xB35975C1u, 0x5B39B997u, 0xBFF2C526u,
|
||||||
|
0xB4256EB5u, 0x71675891u, 0x6FBE1984u, 0x306519F6u,
|
||||||
|
0x08CE4519u, 0xF2357ABEu, 0x3FC05C11u, 0x30C6E91Du,
|
||||||
|
0x7763FDA3u, 0xFDD5D266u, 0x110B6F90u, 0x1F2EFD86u,
|
||||||
|
0x98D90A21u, 0xAE8EDDECu, 0xA2E88E17u, 0xDF6D25D9u,
|
||||||
|
0xB783C519u, 0xFF880B82u, 0x3BF0C612u, 0x2BD6849Cu,
|
||||||
|
0x7354B07Au, 0x020B7961u, 0xEBA8E89Eu, 0x2ED7D4BFu,
|
||||||
|
0x8F438E34u, 0xF14B33E9u, 0xE6FE502Fu, 0xBF986A6Eu,
|
||||||
|
0xA103993Au, 0x27C5B0FFu, 0x3ABB8CA0u, 0x86EDF8D4u,
|
||||||
|
0xD01E172Eu, 0x38F4A865u, 0x0DAE791Au, 0x1C89748Fu,
|
||||||
|
0xEB3E3795u, 0xBFE7D73Bu, 0x4EC6C12Au, 0x877EF600u,
|
||||||
|
0x5A3CBC36u, 0x116030C8u, 0xD5B7A87Cu, 0x524D84D9u,
|
||||||
|
0x23E3E04Fu, 0x78097FA7u, 0xFEC92E57u, 0x7E4DB0C5u,
|
||||||
|
0x3B66D2C0u, 0x2DDEF511u, 0x3ED80C4Bu, 0x13A4087Fu,
|
||||||
|
0x0D5EE881u, 0xAD6AD02Eu, 0x5A542426u, 0x2BDEF8E7u,
|
||||||
|
0x446A7DA7u, 0xFC268A55u, 0x5D9D00BDu, 0x3710D1B5u,
|
||||||
|
0x270F7612u, 0x38F22C86u, 0xFFBFEC26u, 0x9482AA51u,
|
||||||
|
0x8DD6673Bu, 0x8F7C80EFu, 0x5C12531Fu, 0x86AE5611u,
|
||||||
|
0x9CCCD007u, 0x4D29CBF6u, 0x8A0FF3A8u, 0xF0F2332Du,
|
||||||
|
0x275D7034u, 0xDA8F94FDu, 0x5AC736FAu, 0xB4CB60E4u,
|
||||||
|
0x1E74C5A9u, 0x53CC5AC5u, 0xEC538437u, 0x825489D9u,
|
||||||
|
0x0BA43378u, 0x07657513u, 0x35EC8375u, 0x1DA2A732u,
|
||||||
|
0x7A3B5EDEu, 0xAB6FD84Eu, 0x6F8B7EDAu, 0x39994295u,
|
||||||
|
0xD45F7FAFu, 0xBF6AE7C4u, 0xE4257C3Du, 0x5EE315A1u,
|
||||||
|
0x0BB321C5u, 0x0E88401Bu, 0xB7053E8Bu, 0xD25E9808u,
|
||||||
|
0x9FF33EF5u, 0x89A0BD64u, 0xFFDB0F83u, 0xA34404C9u,
|
||||||
|
0x70C36E1Eu, 0x9BE9BABBu, 0x2A932500u, 0x5750FD0Eu,
|
||||||
|
0xA4CAB6F5u, 0x9EC00D66u, 0x1B5F057Du, 0xC88A5A6Bu,
|
||||||
|
0x57E3D177u, 0xBC09B7D8u, 0xB7EBA4D3u, 0x077F3FE7u,
|
||||||
|
0xF8DC24F4u, 0x25E5CF54u, 0xD052AEF5u, 0x30C74026u,
|
||||||
|
0xFD5E2773u, 0xCE327753u, 0xCABD0692u, 0xCF4C8BE0u,
|
||||||
|
0x3AF2851Fu, 0xF2B8CC7Cu, 0x2838C54Bu, 0xBD2729DBu,
|
||||||
|
),
|
||||||
|
uintArrayOf(
|
||||||
|
0xC570A03Cu, 0x1CD9298Du, 0x53AC5593u, 0x5CB35E31u,
|
||||||
|
0xCA7F4500u, 0x868E31F8u, 0x68BF5639u, 0x927BB899u,
|
||||||
|
0x97869F8Cu, 0x22C8AFF2u, 0xE97AB5ACu, 0xC4E199F7u,
|
||||||
|
0x11F56E63u, 0x316E6F9Bu, 0xDC0B25B0u, 0x3C0E37BFu,
|
||||||
|
0x2260AB3Du, 0xC7F5E4FEu, 0x3D408195u, 0x618DC6C1u,
|
||||||
|
0x8801C70Eu, 0xC181139Du, 0xEECFB730u, 0x19F23DE1u,
|
||||||
|
0xD9C4ED07u, 0x6E4C91A3u, 0x4B7131FDu, 0x882FD1B0u,
|
||||||
|
0x95DAC0A1u, 0xC764F41Bu, 0xE8B192A7u, 0x8C8AB9C3u,
|
||||||
|
0x035446CBu, 0xC8655163u, 0xF6CA7757u, 0xFA554923u,
|
||||||
|
0x850ADB81u, 0x9F44293Cu, 0x06742262u, 0x872A79D3u,
|
||||||
|
0xCC79E9FDu, 0xBDAF5759u, 0xA75653BBu, 0x25A1C64Fu,
|
||||||
|
0x33BF5313u, 0xFCC408F4u, 0xC61DBE73u, 0x2DA095D3u,
|
||||||
|
0xDA93F942u, 0x2807D44Au, 0x6663B694u, 0x1383C9A1u,
|
||||||
|
0xFCCF0B6Cu, 0x2EBE6EC6u, 0x3EDF77ECu, 0x066D5D70u,
|
||||||
|
0x6BC67BB5u, 0xC54EE732u, 0xEBE6E605u, 0xA5F5CF9Au,
|
||||||
|
0x3D6C0AB2u, 0x572BE8C0u, 0xF02195C5u, 0xCC75FF05u,
|
||||||
|
0x5454BCE7u, 0xED431C7Au, 0x35FF8D73u, 0xA69F1357u,
|
||||||
|
0xBE3322DFu, 0x8D5701D3u, 0x8227C6E1u, 0x7F92B847u,
|
||||||
|
0x17503B18u, 0xFEBF088Fu, 0xB969378Cu, 0x80695378u,
|
||||||
|
0x6EB6C428u, 0xF6AE7809u, 0xF8115237u, 0x72659F3Du,
|
||||||
|
0x90DD9052u, 0xF60E6B5Bu, 0xE98A45D4u, 0x6CA89B02u,
|
||||||
|
0x85327733u, 0x7C899229u, 0x923FCEDAu, 0x987066D2u,
|
||||||
|
0x3497E625u, 0x3E04C58Du, 0xB1BE8DB6u, 0x172BAEF9u,
|
||||||
|
0x30C3CC5Au, 0x573DDE84u, 0x67F06558u, 0x8E21FF58u,
|
||||||
|
0x00F3F92Du, 0x6CF4CFC2u, 0x13415015u, 0xF461CD1Du,
|
||||||
|
0xAD6C6355u, 0x92BF842Cu, 0x274E705Eu, 0xEE44FD1Du,
|
||||||
|
0x05FD79B4u, 0x40741777u, 0x70A40BF2u, 0x261632C0u,
|
||||||
|
0xD3DAE96Bu, 0xE8EBC9BDu, 0x3BE3D490u, 0xB4530A30u,
|
||||||
|
0xBA6DFDE5u, 0x3A648E2Du, 0xB14C4F26u, 0x7C7D0A3Eu,
|
||||||
|
0x559FB601u, 0x44B1A722u, 0x72FCFF7Fu, 0xF62F6ADFu,
|
||||||
|
0xAEC6F92Eu, 0x6511AD20u, 0x4AF6AD4Au, 0xAA5B3A09u,
|
||||||
|
0x5303B2BEu, 0xBB66DF75u, 0xA2490B13u, 0xEACF61BAu,
|
||||||
|
0x73B29C61u, 0x509A66EEu, 0x8080BDDAu, 0x9216DACAu,
|
||||||
|
0xFAEFC031u, 0x65896009u, 0x3FA36CFCu, 0x995FEFF2u,
|
||||||
|
0xCE98EAB5u, 0x66D7E0CDu, 0xE5A71216u, 0xD182BC77u,
|
||||||
|
0xC7D769A6u, 0xDA5ECC66u, 0x0473072Cu, 0xE84B6CC7u,
|
||||||
|
0x8BBD0177u, 0x0D1075AAu, 0x2BF0168Cu, 0xA7229229u,
|
||||||
|
0xBB80827Bu, 0xF0066C50u, 0x5A614BF6u, 0x23AFE56Au,
|
||||||
|
0x067DAA78u, 0xBF01EEE6u, 0x5B081768u, 0x1CC2F422u,
|
||||||
|
0xFB6A0382u, 0xA5A777A7u, 0x7609E111u, 0x77097C89u,
|
||||||
|
0x075C4FBFu, 0x51E9004Fu, 0xEC84F0CBu, 0x1DE8CC73u,
|
||||||
|
0x2A54A800u, 0x09A89025u, 0xFA5F8045u, 0xC29B195Du,
|
||||||
|
0xCFF9BFE6u, 0x522DBFF5u, 0x9C374800u, 0x347DCD8Fu,
|
||||||
|
0x974DA9C0u, 0x8D6A6D88u, 0xB47EF442u, 0xA51E66CAu,
|
||||||
|
0xA210C54Au, 0x4F63C725u, 0xFF1A465Du, 0x813EB31Bu,
|
||||||
|
0x0E0058D5u, 0x3C18CE5Au, 0xC4D7D98Cu, 0x4E24DA16u,
|
||||||
|
0x5AEC5AF6u, 0x912CD19Fu, 0xB12BF2D1u, 0x184D3B0Bu,
|
||||||
|
0x82DBD6DAu, 0xD29EAD22u, 0x9D13DCE5u, 0xBEA27F78u,
|
||||||
|
0xB957B027u, 0xE0DAE424u, 0x1AE3AB8Fu, 0x49C349A2u,
|
||||||
|
0x74EFDA3Du, 0x88539BD4u, 0xB9F027C3u, 0x5739E997u,
|
||||||
|
0x08D6028Eu, 0x8D1F0B8Fu, 0x63256408u, 0x9B216118u,
|
||||||
|
0xD89432D3u, 0x3BEBF6CAu, 0x21735953u, 0x0EDA4BFBu,
|
||||||
|
0xE6AFC2D4u, 0xA9DB95F9u, 0x1F1C6BB0u, 0xBAAF121Bu,
|
||||||
|
0x8CDC1B36u, 0x3913F9FDu, 0x863BCB1Au, 0xBD34ADCBu,
|
||||||
|
0xDA48457Au, 0x4F584129u, 0xDD85156Cu, 0x0324F396u,
|
||||||
|
0xD41E1EE1u, 0xC3B48F82u, 0x2124FB4Cu, 0x6C0B2635u,
|
||||||
|
0x95CE3157u, 0xA8DACA8Cu, 0xB54E1542u, 0xD989F76Au,
|
||||||
|
0x0C1EA5E3u, 0x973FE85Cu, 0xE6E91D97u, 0x2916B8DFu,
|
||||||
|
0x5B0B05E2u, 0x57BDC906u, 0x7CB2CCEFu, 0x131C7553u,
|
||||||
|
0x41CA9311u, 0x6E70E1C1u, 0x0F972BF2u, 0x8CF59D7Au,
|
||||||
|
0xE6613022u, 0x69218E19u, 0xC350744Au, 0xA2D5BF1Bu,
|
||||||
|
0x9FA14B7Bu, 0x8867F25Cu, 0xB8E19AF6u, 0x777A25DFu,
|
||||||
|
0xA2004E28u, 0xFB929664u, 0x2DA8A284u, 0x21955D47u,
|
||||||
|
0xF0CEBD06u, 0x9887B1E9u, 0xBF7810C1u, 0x265D91F9u,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(P_ARRAY.size == 18)
|
||||||
|
check(S_BOXES.size == 4)
|
||||||
|
|
||||||
|
for (box in S_BOXES) {
|
||||||
|
check(box.size == 256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package world.phantasmal.psoserv.encryption
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
|
||||||
|
interface Cipher {
|
||||||
|
val blockSize: Int
|
||||||
|
val key: ByteArray
|
||||||
|
|
||||||
|
fun encrypt(data: Buffer, offset: Int = 0, blocks: Int = data.size / blockSize)
|
||||||
|
fun decrypt(data: Buffer, offset: Int = 0, blocks: Int = data.size / blockSize)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance by the given number of blocks, used by stateful ciphers such as the PC cipher.
|
||||||
|
*/
|
||||||
|
fun advance(blocks: Int)
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
package world.phantasmal.psoserv.encryption
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.Endianness
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import java.lang.Integer.divideUnsigned
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class PcCipher(override val key: ByteArray = createKey()) : Cipher {
|
||||||
|
private val subKeys: IntArray
|
||||||
|
private var position = 56
|
||||||
|
|
||||||
|
override val blockSize: Int = 4
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(key.size == KEY_SIZE) {
|
||||||
|
"Expected key of size ${KEY_SIZE}, but was ${key.size}."
|
||||||
|
}
|
||||||
|
|
||||||
|
val intKey = Buffer.fromByteArray(key, Endianness.Little).getInt(0)
|
||||||
|
subKeys = createSubKeys(intKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encrypt(data: Buffer, offset: Int, blocks: Int) {
|
||||||
|
require(offset >= 0)
|
||||||
|
require(blocks >= 0)
|
||||||
|
val limit = offset + blocks * blockSize
|
||||||
|
require(limit <= data.size)
|
||||||
|
|
||||||
|
var i = offset
|
||||||
|
|
||||||
|
while (i < limit) {
|
||||||
|
data.setInt(i, data.getInt(i) xor getNextKey())
|
||||||
|
i += blockSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decrypt(data: Buffer, offset: Int, blocks: Int) {
|
||||||
|
encrypt(data, offset, blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun advance(blocks: Int) {
|
||||||
|
repeat(blocks) {
|
||||||
|
getNextKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNextKey(): Int {
|
||||||
|
if (position == 56) {
|
||||||
|
mixKeys(subKeys)
|
||||||
|
position = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return subKeys[position++]
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_SIZE: Int = 4
|
||||||
|
|
||||||
|
fun createKey(): ByteArray = Random.nextBytes(KEY_SIZE)
|
||||||
|
|
||||||
|
private fun mixKeys(subKeys: IntArray) {
|
||||||
|
var esi: Int
|
||||||
|
var ebp: Int
|
||||||
|
var edi = 1
|
||||||
|
var edx = 0x18
|
||||||
|
var eax = edi
|
||||||
|
|
||||||
|
while (edx > 0) {
|
||||||
|
esi = subKeys[eax + 0x1F]
|
||||||
|
ebp = subKeys[eax]
|
||||||
|
ebp -= esi
|
||||||
|
subKeys[eax] = ebp
|
||||||
|
eax++
|
||||||
|
edx--
|
||||||
|
}
|
||||||
|
|
||||||
|
edi = 0x19
|
||||||
|
edx = 0x1F
|
||||||
|
eax = edi
|
||||||
|
|
||||||
|
while (edx > 0) {
|
||||||
|
esi = subKeys[eax - 0x18]
|
||||||
|
ebp = subKeys[eax]
|
||||||
|
ebp -= esi
|
||||||
|
subKeys[eax] = ebp
|
||||||
|
eax++
|
||||||
|
edx--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSubKeys(key: Int): IntArray {
|
||||||
|
val subKeys = IntArray(57)
|
||||||
|
var eax: Int
|
||||||
|
var edx: Int
|
||||||
|
var var1: Int
|
||||||
|
var esi = 1
|
||||||
|
var ebx = key
|
||||||
|
var edi = 0x15
|
||||||
|
subKeys[56] = ebx
|
||||||
|
subKeys[55] = ebx
|
||||||
|
|
||||||
|
while (edi <= 0x46E) {
|
||||||
|
eax = edi
|
||||||
|
var1 = divideUnsigned(eax, 55)
|
||||||
|
edx = eax - var1 * 55
|
||||||
|
ebx -= esi
|
||||||
|
edi += 0x15
|
||||||
|
subKeys[edx] = esi
|
||||||
|
esi = ebx
|
||||||
|
ebx = subKeys[edx]
|
||||||
|
}
|
||||||
|
|
||||||
|
mixKeys(subKeys)
|
||||||
|
mixKeys(subKeys)
|
||||||
|
mixKeys(subKeys)
|
||||||
|
mixKeys(subKeys)
|
||||||
|
|
||||||
|
return subKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,358 @@
|
|||||||
|
package world.phantasmal.psoserv.messages
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import world.phantasmal.psolib.cursor.Cursor
|
||||||
|
import world.phantasmal.psolib.cursor.WritableCursor
|
||||||
|
import world.phantasmal.psolib.cursor.cursor
|
||||||
|
|
||||||
|
private const val INIT_MSG_SIZE: Int = 96
|
||||||
|
private const val KEY_SIZE: Int = 48
|
||||||
|
|
||||||
|
const val BB_HEADER_SIZE: Int = 8
|
||||||
|
const val BB_MSG_SIZE_POS: Int = 0
|
||||||
|
const val BB_MSG_CODE_POS: Int = 2
|
||||||
|
|
||||||
|
enum class BbAuthenticationStatus {
|
||||||
|
Success, Error, UnknownUser
|
||||||
|
}
|
||||||
|
|
||||||
|
class PsoCharacter(
|
||||||
|
val slot: Int,
|
||||||
|
val exp: Int,
|
||||||
|
val level: Int,
|
||||||
|
val guildCardString: String,
|
||||||
|
val nameColor: Int,
|
||||||
|
val model: Int,
|
||||||
|
val nameColorChecksum: Int,
|
||||||
|
val sectionId: Int,
|
||||||
|
val characterClass: Int,
|
||||||
|
val costume: Int,
|
||||||
|
val skin: Int,
|
||||||
|
val face: Int,
|
||||||
|
val head: Int,
|
||||||
|
val hair: Int,
|
||||||
|
val hairRed: Int,
|
||||||
|
val hairGreen: Int,
|
||||||
|
val hairBlue: Int,
|
||||||
|
val propX: Double,
|
||||||
|
val propY: Double,
|
||||||
|
val name: String,
|
||||||
|
val playTime: Int,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(slot in 0..3)
|
||||||
|
require(exp >= 0)
|
||||||
|
require(level in 1..200)
|
||||||
|
require(guildCardString.length <= 16)
|
||||||
|
require(name.length <= 16)
|
||||||
|
require(playTime >= 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuildCardEntry(
|
||||||
|
val playerTag: Int,
|
||||||
|
val serialNumber: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val sectionId: Int,
|
||||||
|
val characterClass: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
class GuildCard(
|
||||||
|
val entries: List<GuildCardEntry>
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class BbMessage(override val buffer: Buffer) : Message(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()
|
||||||
|
|
||||||
|
class InitEncryption(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val serverKey: ByteArray get() = byteArray(INIT_MSG_SIZE, size = KEY_SIZE)
|
||||||
|
val clientKey: ByteArray get() = byteArray(INIT_MSG_SIZE + KEY_SIZE, size = KEY_SIZE)
|
||||||
|
|
||||||
|
constructor(message: String, serverKey: ByteArray, clientKey: ByteArray) : this(
|
||||||
|
buf(0x0003, INIT_MSG_SIZE + 2 * KEY_SIZE) {
|
||||||
|
require(message.length <= INIT_MSG_SIZE)
|
||||||
|
require(serverKey.size == KEY_SIZE)
|
||||||
|
require(clientKey.size == KEY_SIZE)
|
||||||
|
|
||||||
|
writeStringAscii(message, byteLength = INIT_MSG_SIZE)
|
||||||
|
writeByteArray(serverKey)
|
||||||
|
writeByteArray(clientKey)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Disconnect(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x0005))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Redirect(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
var ipAddress: ByteArray
|
||||||
|
get() = byteArray(0, size = 4)
|
||||||
|
set(value) {
|
||||||
|
require(value.size == 4)
|
||||||
|
setByteArray(0, value)
|
||||||
|
}
|
||||||
|
var port: Int
|
||||||
|
get() = uShort(4).toInt()
|
||||||
|
set(value) {
|
||||||
|
require(value in 0..65535)
|
||||||
|
setShort(4, value.toShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(ipAddress: ByteArray, port: Int) : this(
|
||||||
|
buf(0x0019, 8) {
|
||||||
|
require(ipAddress.size == 4)
|
||||||
|
require(port in 0..65535)
|
||||||
|
|
||||||
|
writeByteArray(ipAddress)
|
||||||
|
writeShort(port.toShort())
|
||||||
|
writeShort(0) // Padding.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
messageString(
|
||||||
|
"ipAddress" to ipAddress.joinToString(".") { it.toUByte().toString() },
|
||||||
|
"port" to port,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x0093
|
||||||
|
// Also contains ignored tag, hardware info and security data.
|
||||||
|
class Authenticate(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val guildCard: Int get() = int(4)
|
||||||
|
val version: Short get() = short(8)
|
||||||
|
val teamId: Int get() = int(16)
|
||||||
|
val userName: String
|
||||||
|
get() = stringAscii(offset = 20, maxByteLength = 16, nullTerminated = true)
|
||||||
|
val password: String
|
||||||
|
get() = stringAscii(offset = 68, maxByteLength = 16, nullTerminated = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuildCardHeader(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val guildCardSize: Int get() = int(4)
|
||||||
|
val checksum: Int get() = int(8)
|
||||||
|
|
||||||
|
constructor(guildCardSize: Int, checksum: Int) : this(
|
||||||
|
buf(0x01DC, 12) {
|
||||||
|
writeInt(1)
|
||||||
|
writeInt(guildCardSize)
|
||||||
|
writeInt(checksum)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
messageString("guildCardSize" to guildCardSize, "checksum" to checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuildCardChunk(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val chunkNo: Int get() = int(4)
|
||||||
|
|
||||||
|
constructor(chunkNo: Int, chunk: Cursor) : this(
|
||||||
|
buf(0x02DC, 8 + chunk.size) {
|
||||||
|
writeInt(0)
|
||||||
|
writeInt(chunkNo)
|
||||||
|
writeCursor(chunk)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
messageString("chunkNo" to chunkNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x03DC
|
||||||
|
class GetGuildCardChunk(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val chunkNo: Int get() = int(4)
|
||||||
|
val cont: Boolean get() = int(8) != 0
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
messageString("chunkNo" to chunkNo, "cont" to cont)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x00E0
|
||||||
|
class GetAccount(buffer: Buffer) : BbMessage(buffer)
|
||||||
|
|
||||||
|
class Account(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(guildCard: Int, teamId: Int) : this(
|
||||||
|
buf(0x00E2, 2804) {
|
||||||
|
// 276 Bytes of unknown data.
|
||||||
|
repeat(69) { writeInt(0) }
|
||||||
|
writeByteArray(DEFAULT_KEYBOARD_GAMEPAD_CONFIG)
|
||||||
|
writeInt(guildCard)
|
||||||
|
writeInt(teamId)
|
||||||
|
// 2092 Bytes of team data.
|
||||||
|
repeat(523) { writeInt(0) }
|
||||||
|
// Enable all team rewards.
|
||||||
|
writeUInt(UInt.MAX_VALUE)
|
||||||
|
writeUInt(UInt.MAX_VALUE)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x00E3
|
||||||
|
class CharacterSelect(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
val slot: Int get() = uByte(0).toInt()
|
||||||
|
val select: Boolean get() = byte(4).toInt() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class CharacterSelectResponse(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(char: PsoCharacter) : this(
|
||||||
|
buf(0x00E5, 128) {
|
||||||
|
writeInt(char.slot)
|
||||||
|
writeInt(char.exp)
|
||||||
|
writeInt(char.level)
|
||||||
|
writeStringAscii(char.guildCardString, byteLength = 16)
|
||||||
|
repeat(2) { writeInt(0) } // Unknown.
|
||||||
|
writeInt(char.nameColor)
|
||||||
|
writeInt(char.model)
|
||||||
|
repeat(3) { writeInt(0) } // Unused.
|
||||||
|
writeInt(char.nameColorChecksum)
|
||||||
|
writeByte(char.sectionId.toByte())
|
||||||
|
writeByte(char.characterClass.toByte())
|
||||||
|
writeByte(0) // V2 flags.
|
||||||
|
writeByte(0) // Version.
|
||||||
|
writeInt(0) // V1 flags.
|
||||||
|
writeShort(char.costume.toShort())
|
||||||
|
writeShort(char.skin.toShort())
|
||||||
|
writeShort(char.face.toShort())
|
||||||
|
writeShort(char.head.toShort())
|
||||||
|
writeShort(char.hair.toShort())
|
||||||
|
writeShort(char.hairRed.toShort())
|
||||||
|
writeShort(char.hairGreen.toShort())
|
||||||
|
writeShort(char.hairBlue.toShort())
|
||||||
|
writeFloat(char.propX.toFloat())
|
||||||
|
writeFloat(char.propY.toFloat())
|
||||||
|
writeStringUtf16(char.name, byteLength = 32)
|
||||||
|
writeInt(char.playTime)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationResponse(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(status: BbAuthenticationStatus, guildCard: Int, teamId: Int) : this(
|
||||||
|
buf(0x00E6, 60) {
|
||||||
|
writeInt(
|
||||||
|
when (status) {
|
||||||
|
BbAuthenticationStatus.Success -> 0
|
||||||
|
BbAuthenticationStatus.Error -> 1
|
||||||
|
BbAuthenticationStatus.UnknownUser -> 8
|
||||||
|
}
|
||||||
|
)
|
||||||
|
writeInt(0x10000)
|
||||||
|
writeInt(guildCard)
|
||||||
|
writeInt(teamId)
|
||||||
|
writeInt(
|
||||||
|
if (status == BbAuthenticationStatus.Success) (0xDEADBEEF).toInt() else 0
|
||||||
|
)
|
||||||
|
// 36 Bytes of unknown data.
|
||||||
|
repeat(9) { writeInt(0) }
|
||||||
|
writeInt(0x102)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Checksum(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(checksum: Int) : this(
|
||||||
|
buf(0x01E8, 8) {
|
||||||
|
writeInt(checksum)
|
||||||
|
writeInt(0) // Padding.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val checksum: Int get() = int(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChecksumResponse(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(success: Boolean) : this(
|
||||||
|
buf(0x02E8, 4) {
|
||||||
|
writeInt(if (success) 1 else 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetGuildCardHeader(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x03E8))
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileList(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x01EB))
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileChunk(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor(chunkNo: Int, chunk: Cursor) : this(
|
||||||
|
buf(0x02EB, 4 + chunk.size) {
|
||||||
|
writeInt(chunkNo)
|
||||||
|
writeCursor(chunk)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetFileChunk(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x03EB))
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetFileList(buffer: Buffer) : BbMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x04EB))
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
writeBody: WritableCursor.() -> Unit = {},
|
||||||
|
): Buffer {
|
||||||
|
val size = BB_HEADER_SIZE + bodySize
|
||||||
|
val buffer = Buffer.withSize(size)
|
||||||
|
|
||||||
|
val cursor = buffer.cursor()
|
||||||
|
// Write header.
|
||||||
|
.writeShort(size.toShort())
|
||||||
|
.writeShort(code.toShort())
|
||||||
|
.writeInt(0) // Flags.
|
||||||
|
|
||||||
|
cursor.writeBody()
|
||||||
|
|
||||||
|
require(cursor.position == buffer.size) {
|
||||||
|
"Message buffer should be filled completely, only ${cursor.position} / ${buffer.size} bytes written."
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package world.phantasmal.psoserv.messages
|
||||||
|
|
||||||
|
val DEFAULT_KEYBOARD_GAMEPAD_CONFIG: ByteArray = ubyteArrayOf(
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x26u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x22u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x10u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x13u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x61u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x50u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x06u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x59u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x5eu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x5du, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x5cu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x5fu, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x07u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x5du, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x5cu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x5fu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x56u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x5eu, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x40u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x41u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x42u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x43u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x44u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x45u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x46u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x47u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x48u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x49u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x4au, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x4bu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x2au, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x2bu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x2cu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x2du, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x2eu, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x2fu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x30u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x31u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x32u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x33u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0xffu, 0xffu, 0x00u, 0x00u,
|
||||||
|
0x01u, 0x00u, 0x00u, 0x00u, 0x02u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x08u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x02u, 0x00u, 0x00u, 0x00u, 0x08u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x00u, 0x02u, 0x00u, 0x00u, 0x20u, 0x00u, 0x00u, 0x00u, 0x80u, 0x00u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||||
|
).asByteArray()
|
@ -0,0 +1,62 @@
|
|||||||
|
package world.phantasmal.psoserv.messages
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
|
||||||
|
fun messageString(
|
||||||
|
code: Int,
|
||||||
|
size: Int,
|
||||||
|
name: String? = null,
|
||||||
|
vararg props: Pair<String, Any>,
|
||||||
|
): String =
|
||||||
|
buildString {
|
||||||
|
append(name ?: "Message")
|
||||||
|
append("[0x")
|
||||||
|
append(code.toString(16).uppercase().padStart(4, '0'))
|
||||||
|
append(",size=")
|
||||||
|
append(size)
|
||||||
|
|
||||||
|
if (props.isNotEmpty()) {
|
||||||
|
props.joinTo(this, prefix = ",", separator = ",") { (prop, value) -> "$prop=$value" }
|
||||||
|
}
|
||||||
|
|
||||||
|
append("]")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Header(val code: Int, val size: Int)
|
||||||
|
|
||||||
|
abstract class Message(val headerSize: Int) {
|
||||||
|
abstract val buffer: Buffer
|
||||||
|
abstract val code: Int
|
||||||
|
abstract val size: Int
|
||||||
|
val bodySize: Int get() = size - headerSize
|
||||||
|
|
||||||
|
override fun toString(): String = messageString()
|
||||||
|
|
||||||
|
protected fun uByte(offset: Int) = buffer.getUByte(headerSize + offset)
|
||||||
|
protected fun uShort(offset: Int) = buffer.getUShort(headerSize + offset)
|
||||||
|
protected fun uInt(offset: Int) = buffer.getUInt(headerSize + offset)
|
||||||
|
protected fun byte(offset: Int) = buffer.getByte(headerSize + offset)
|
||||||
|
protected fun short(offset: Int) = buffer.getShort(headerSize + offset)
|
||||||
|
protected fun int(offset: Int) = buffer.getInt(headerSize + offset)
|
||||||
|
protected fun byteArray(offset: Int, size: Int) = ByteArray(size) { byte(offset + it) }
|
||||||
|
protected fun stringAscii(offset: Int, maxByteLength: Int, nullTerminated: Boolean) =
|
||||||
|
buffer.getStringAscii(headerSize + offset, maxByteLength, nullTerminated)
|
||||||
|
|
||||||
|
protected fun setByte(offset: Int, value: Byte) {
|
||||||
|
buffer.setByte(headerSize + offset, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setShort(offset: Int, value: Short) {
|
||||||
|
buffer.setShort(headerSize + offset, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setByteArray(offset: Int, array: ByteArray) {
|
||||||
|
for ((index, byte) in array.withIndex()) {
|
||||||
|
setByte(offset + index, byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun messageString(vararg props: Pair<String, Any>): String =
|
||||||
|
messageString(code, size, this::class.simpleName, *props)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
package world.phantasmal.psoserv.messages
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.Endianness
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import world.phantasmal.psolib.cursor.WritableCursor
|
||||||
|
import world.phantasmal.psolib.cursor.cursor
|
||||||
|
import world.phantasmal.psoserv.roundToBlockSize
|
||||||
|
|
||||||
|
private const val INIT_MSG_SIZE: Int = 64
|
||||||
|
private const val KEY_SIZE: Int = 4
|
||||||
|
|
||||||
|
const val PC_HEADER_SIZE: Int = 4
|
||||||
|
const val PC_MSG_SIZE_POS: Int = 0
|
||||||
|
const val PC_MSG_CODE_POS: Int = 2
|
||||||
|
|
||||||
|
sealed class PcMessage(override val buffer: Buffer) : Message(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()
|
||||||
|
|
||||||
|
class InitEncryption(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
val serverKey: ByteArray get() = byteArray(INIT_MSG_SIZE, size = KEY_SIZE)
|
||||||
|
val clientKey: ByteArray get() = byteArray(INIT_MSG_SIZE + KEY_SIZE, size = KEY_SIZE)
|
||||||
|
|
||||||
|
constructor(message: String, serverKey: ByteArray, clientKey: ByteArray) : this(
|
||||||
|
buf(0x02, INIT_MSG_SIZE + 2 * KEY_SIZE) {
|
||||||
|
require(message.length <= INIT_MSG_SIZE)
|
||||||
|
require(serverKey.size == KEY_SIZE)
|
||||||
|
require(clientKey.size == KEY_SIZE)
|
||||||
|
|
||||||
|
writeStringAscii(message, byteLength = INIT_MSG_SIZE)
|
||||||
|
writeByteArray(serverKey)
|
||||||
|
writeByteArray(clientKey)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Login(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x04))
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchListStart(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x0B))
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchListEnd(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x0D))
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchDone(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
constructor() : this(buf(0x12))
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchListOk(buffer: Buffer) : PcMessage(buffer)
|
||||||
|
|
||||||
|
class WelcomeMessage(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
constructor(message: String) : this(
|
||||||
|
buf(0x13, roundToBlockSize(2 * message.length, 4)) {
|
||||||
|
writeStringUtf16(message, roundToBlockSize(2 * message.length, 4))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Redirect(buffer: Buffer) : PcMessage(buffer) {
|
||||||
|
var ipAddress: ByteArray
|
||||||
|
get() = byteArray(0, size = 4)
|
||||||
|
set(value) {
|
||||||
|
require(value.size == 4)
|
||||||
|
setByteArray(0, value)
|
||||||
|
}
|
||||||
|
var port: Int
|
||||||
|
get() {
|
||||||
|
buffer.endianness = Endianness.Big
|
||||||
|
val p = uShort(4).toInt()
|
||||||
|
buffer.endianness = Endianness.Little
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
require(value in 0..65535)
|
||||||
|
buffer.endianness = Endianness.Big
|
||||||
|
setShort(4, value.toShort())
|
||||||
|
buffer.endianness = Endianness.Little
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(ipAddress: ByteArray, port: Int) : this(
|
||||||
|
buf(0x14, 8) {
|
||||||
|
require(ipAddress.size == 4)
|
||||||
|
require(port in 0..65535)
|
||||||
|
|
||||||
|
writeByteArray(ipAddress)
|
||||||
|
endianness = Endianness.Big
|
||||||
|
writeShort(port.toShort())
|
||||||
|
endianness = Endianness.Little
|
||||||
|
writeShort(0) // Padding.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
messageString(
|
||||||
|
"ipAddress" to ipAddress.joinToString(".") { it.toUByte().toString() },
|
||||||
|
"port" to port,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
writeBody: WritableCursor.() -> Unit = {},
|
||||||
|
): Buffer {
|
||||||
|
val size = PC_HEADER_SIZE + bodySize
|
||||||
|
val buffer = Buffer.withSize(size)
|
||||||
|
|
||||||
|
val cursor = buffer.cursor()
|
||||||
|
// Write Header
|
||||||
|
.writeShort(size.toShort())
|
||||||
|
.writeByte(code.toByte())
|
||||||
|
.writeByte(0) // Flags
|
||||||
|
|
||||||
|
cursor.writeBody()
|
||||||
|
|
||||||
|
require(cursor.position == buffer.size) {
|
||||||
|
"Message buffer should be filled completely, only ${cursor.position} / ${buffer.size} bytes written."
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package world.phantasmal.psoserv.servers
|
||||||
|
|
||||||
|
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.Header
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
abstract class BbServer<StateType : ServerState<BbMessage, StateType>>(
|
||||||
|
logger: KLogger,
|
||||||
|
address: InetAddress,
|
||||||
|
port: Int,
|
||||||
|
) : Server<BbMessage, StateType>(logger, address, port) {
|
||||||
|
|
||||||
|
override fun createCipher() = BbCipher()
|
||||||
|
|
||||||
|
override fun readHeader(buffer: Buffer): Header =
|
||||||
|
BbMessage.readHeader(buffer)
|
||||||
|
|
||||||
|
override fun readMessage(buffer: Buffer): BbMessage =
|
||||||
|
BbMessage.fromBuffer(buffer)
|
||||||
|
}
|
@ -0,0 +1,241 @@
|
|||||||
|
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.BbMessage
|
||||||
|
import world.phantasmal.psoserv.messages.Header
|
||||||
|
import world.phantasmal.psoserv.messages.Message
|
||||||
|
import world.phantasmal.psoserv.messages.PcMessage
|
||||||
|
import java.net.*
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var running = true
|
||||||
|
private var connected = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
LOGGER.info { "Initializing." }
|
||||||
|
|
||||||
|
// Accept client connections on a dedicated thread.
|
||||||
|
val thread = Thread(::acceptConnections)
|
||||||
|
thread.name = this::class.simpleName
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
LOGGER.info { "Stopping." }
|
||||||
|
|
||||||
|
// Signal to the connection thread that it should stop.
|
||||||
|
running = false
|
||||||
|
|
||||||
|
// 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." }
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
val clientSocket = proxySocket.accept()
|
||||||
|
LOGGER.info {
|
||||||
|
"New client connection from ${clientSocket.inetAddress}:${clientSocket.port}."
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverSocket = Socket(serverAddress, serverPort)
|
||||||
|
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()
|
||||||
|
} catch (e: SocketTimeoutException) {
|
||||||
|
// Retry after timeout.
|
||||||
|
continue
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
LOGGER.error(e) {
|
||||||
|
"Interrupted while trying to accept client connections on $proxyAddress:$proxyPort, 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LOGGER.error(e) {
|
||||||
|
"Exception while trying to accept client connections on $proxyAddress:$proxyPort."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info { "Stopped." }
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ServerHandler(
|
||||||
|
serverSocket: Socket,
|
||||||
|
private val clientSocket: Socket,
|
||||||
|
) : SocketHandler<Message>(KotlinLogging.logger {}, serverSocket, headerSize) {
|
||||||
|
|
||||||
|
private var clientHandler: ClientHandler? = null
|
||||||
|
|
||||||
|
// 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 PcMessage.InitEncryption -> if (decryptCipher == null) {
|
||||||
|
decryptCipher = createCipher(message.serverKey)
|
||||||
|
encryptCipher = createCipher(message.serverKey)
|
||||||
|
|
||||||
|
val clientDecryptCipher = createCipher(message.clientKey)
|
||||||
|
val clientEncryptCipher = createCipher(message.clientKey)
|
||||||
|
|
||||||
|
logger.info {
|
||||||
|
"Encryption initialized, start listening to client."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening to client on another thread.
|
||||||
|
val clientListener = ClientHandler(
|
||||||
|
clientSocket,
|
||||||
|
this,
|
||||||
|
clientDecryptCipher,
|
||||||
|
clientEncryptCipher
|
||||||
|
)
|
||||||
|
this.clientHandler = clientListener
|
||||||
|
val thread = Thread(clientListener::listen)
|
||||||
|
thread.name = "${ProxyServer::class.simpleName} client"
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.InitEncryption -> if (decryptCipher == null) {
|
||||||
|
decryptCipher = createCipher(message.serverKey)
|
||||||
|
encryptCipher = createCipher(message.serverKey)
|
||||||
|
|
||||||
|
val clientDecryptCipher = createCipher(message.clientKey)
|
||||||
|
val clientEncryptCipher = createCipher(message.clientKey)
|
||||||
|
|
||||||
|
logger.info {
|
||||||
|
"Encryption initialized, start listening to client."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening to client on another thread.
|
||||||
|
val clientListener = ClientHandler(
|
||||||
|
clientSocket,
|
||||||
|
this,
|
||||||
|
clientDecryptCipher,
|
||||||
|
clientEncryptCipher
|
||||||
|
)
|
||||||
|
this.clientHandler = clientListener
|
||||||
|
val thread = Thread(clientListener::listen)
|
||||||
|
thread.name = "${ProxyServer::class.simpleName} client"
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PcMessage.Redirect -> {
|
||||||
|
val oldAddress = InetAddress.getByAddress(message.ipAddress)
|
||||||
|
|
||||||
|
redirectMap[Pair(oldAddress, message.port)]?.let { (newAddress, newPort) ->
|
||||||
|
logger.debug {
|
||||||
|
"Rewriting redirect from $oldAddress:${message.port} to $newAddress:$newPort."
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ipAddress = newAddress.address
|
||||||
|
message.port = newPort
|
||||||
|
|
||||||
|
return ProcessResult.Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.Redirect -> {
|
||||||
|
val oldAddress = InetAddress.getByAddress(message.ipAddress)
|
||||||
|
|
||||||
|
redirectMap[Pair(oldAddress, message.port)]?.let { (newAddress, newPort) ->
|
||||||
|
logger.debug {
|
||||||
|
"Rewriting redirect from $oldAddress:${message.port} to $newAddress:$newPort."
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ipAddress = newAddress.address
|
||||||
|
message.port = newPort
|
||||||
|
|
||||||
|
return ProcessResult.Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessResult.Ok
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processRawBytes(buffer: Buffer, offset: Int, size: Int) {
|
||||||
|
clientHandler?.writeBytes(buffer, offset, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun socketClosed() {
|
||||||
|
clientHandler?.stop()
|
||||||
|
clientHandler = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ClientHandler(
|
||||||
|
clientSocket: Socket,
|
||||||
|
private val serverHandler: ServerHandler,
|
||||||
|
override val decryptCipher: Cipher,
|
||||||
|
override val encryptCipher: Cipher,
|
||||||
|
) : SocketHandler<Message>(KotlinLogging.logger {}, clientSocket, headerSize) {
|
||||||
|
|
||||||
|
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 = ProcessResult.Ok
|
||||||
|
|
||||||
|
override fun processRawBytes(buffer: Buffer, offset: Int, size: Int) {
|
||||||
|
serverHandler.writeBytes(buffer, offset, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun socketClosed() {
|
||||||
|
serverHandler.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOGGER = KotlinLogging.logger {}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,225 @@
|
|||||||
|
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
|
||||||
|
import world.phantasmal.psoserv.messages.Header
|
||||||
|
import world.phantasmal.psoserv.messages.Message
|
||||||
|
import world.phantasmal.psoserv.messages.messageString
|
||||||
|
import world.phantasmal.psoserv.roundToBlockSize
|
||||||
|
import java.net.*
|
||||||
|
|
||||||
|
private const val MAX_MSG_SIZE: Int = 32768
|
||||||
|
|
||||||
|
abstract class Server<MessageType : Message, StateType : ServerState<MessageType, StateType>>(
|
||||||
|
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." }
|
||||||
|
|
||||||
|
// Accept client connections on a dedicated thread.
|
||||||
|
val thread = Thread(::acceptConnections)
|
||||||
|
thread.name = this::class.simpleName
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
logger.info { "Stopping." }
|
||||||
|
|
||||||
|
// Signal to the connection thread that it should stop.
|
||||||
|
running = false
|
||||||
|
|
||||||
|
// Closing the server socket will generate a SocketException on the connection thread which
|
||||||
|
// will then shut down.
|
||||||
|
serverSocket.close()
|
||||||
|
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acceptConnections() {
|
||||||
|
if (running) {
|
||||||
|
logger.info { "Accepting connections." }
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
val socket = serverSocket.accept()
|
||||||
|
logger.info { "New client connection from ${socket.inetAddress}." }
|
||||||
|
// Handle each client connection in its own thread.
|
||||||
|
// TODO: Shut down client threads when server is stopped.
|
||||||
|
val thread = Thread { clientConnected(socket) }
|
||||||
|
thread.name = "${this::class.simpleName} client ${connectionCounter++}"
|
||||||
|
thread.start()
|
||||||
|
} catch (e: SocketTimeoutException) {
|
||||||
|
// Retry after timeout.
|
||||||
|
continue
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
logger.error(e) {
|
||||||
|
"Interrupted while trying to accept client connections on $address:$port, 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 $address:$port, stopping."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.error(e) {
|
||||||
|
"Exception while trying to accept client connections on $address:$port."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Stopped." }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun clientConnected(socket: Socket) {
|
||||||
|
try {
|
||||||
|
val readBuffer = Buffer.withSize(MAX_MSG_SIZE, Endianness.Little)
|
||||||
|
|
||||||
|
var read = 0
|
||||||
|
var maxRead = MAX_MSG_SIZE
|
||||||
|
var headerDecrypted = false
|
||||||
|
|
||||||
|
val serverCipher = createCipher()
|
||||||
|
val clientCipher = createCipher()
|
||||||
|
|
||||||
|
val sender = ClientSender(logger, socket, serverCipher, clientCipher)
|
||||||
|
|
||||||
|
var state: StateType = initializeState(sender)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val readNow = socket.getInputStream().read(readBuffer.byteArray, read, maxRead)
|
||||||
|
|
||||||
|
if (readNow == -1) {
|
||||||
|
// Close the connection.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
read += readNow
|
||||||
|
maxRead = MAX_MSG_SIZE - read
|
||||||
|
|
||||||
|
if (read >= clientCipher.blockSize) {
|
||||||
|
if (!headerDecrypted) {
|
||||||
|
clientCipher.decrypt(readBuffer, offset = 0, blocks = 1)
|
||||||
|
headerDecrypted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header size is always equal to cipher block size, so we can read it at this
|
||||||
|
// point.
|
||||||
|
val (code, size) = readHeader(readBuffer)
|
||||||
|
|
||||||
|
when {
|
||||||
|
size > MAX_MSG_SIZE -> {
|
||||||
|
logger.error {
|
||||||
|
val message = messageString(code, size)
|
||||||
|
"Receiving $message, too large: ${size}B."
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
read >= size -> {
|
||||||
|
val messageBuffer = readBuffer.copy(size = size)
|
||||||
|
|
||||||
|
read -= size
|
||||||
|
maxRead = MAX_MSG_SIZE - read
|
||||||
|
headerDecrypted = false
|
||||||
|
|
||||||
|
// Shift any remaining bytes to the front of the buffer.
|
||||||
|
if (read > 0) {
|
||||||
|
readBuffer.copyInto(readBuffer, offset = size, size = read)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCipher.decrypt(
|
||||||
|
messageBuffer,
|
||||||
|
offset = clientCipher.blockSize,
|
||||||
|
blocks = size / clientCipher.blockSize - 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
val message = readMessage(messageBuffer)
|
||||||
|
logger.trace { "Received $message." }
|
||||||
|
|
||||||
|
state = state.process(message)
|
||||||
|
|
||||||
|
if (state is FinalServerState) {
|
||||||
|
// Give the client some time to disconnect.
|
||||||
|
Thread.sleep(100)
|
||||||
|
|
||||||
|
// Close the connection.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
maxRead = size - read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.error(e) {
|
||||||
|
"Error while processing client connection from ${socket.inetAddress}."
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
logger.info { "Closing client connection from ${socket.inetAddress}." }
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun createCipher(): Cipher
|
||||||
|
|
||||||
|
protected abstract fun initializeState(sender: ClientSender): StateType
|
||||||
|
|
||||||
|
protected abstract fun readHeader(buffer: Buffer): Header
|
||||||
|
|
||||||
|
protected abstract fun readMessage(buffer: Buffer): MessageType
|
||||||
|
|
||||||
|
class ClientSender(
|
||||||
|
private val logger: KLogger,
|
||||||
|
private val socket: Socket,
|
||||||
|
val serverCipher: Cipher,
|
||||||
|
val clientCipher: Cipher,
|
||||||
|
) {
|
||||||
|
fun send(message: Message, encrypt: Boolean = true) {
|
||||||
|
logger.trace {
|
||||||
|
"Sending $message${if (encrypt) "" else " (unencrypted)"}."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.buffer.size != message.size) {
|
||||||
|
logger.warn {
|
||||||
|
"Message size of $message is ${message.size}B, but wrote ${message.buffer.size} bytes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val buffer: Buffer
|
||||||
|
|
||||||
|
if (encrypt) {
|
||||||
|
// Pad buffer before encrypting.
|
||||||
|
val initialSize = message.buffer.size
|
||||||
|
buffer = message.buffer.copy(
|
||||||
|
size = roundToBlockSize(initialSize, serverCipher.blockSize)
|
||||||
|
)
|
||||||
|
serverCipher.encrypt(buffer)
|
||||||
|
} else {
|
||||||
|
buffer = message.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.getOutputStream().write(buffer.byteArray, 0, buffer.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package world.phantasmal.psoserv.servers
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import world.phantasmal.psoserv.messages.Message
|
||||||
|
|
||||||
|
abstract class ServerState<ClientMsgType : Message, Self : ServerState<ClientMsgType, Self>> {
|
||||||
|
private val logger = KotlinLogging.logger(this::class.qualifiedName!!)
|
||||||
|
|
||||||
|
init {
|
||||||
|
logger.trace { "Transitioning to ${this::class.simpleName}." }
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun process(message: ClientMsgType): Self
|
||||||
|
|
||||||
|
protected fun unexpectedMessage(message: ClientMsgType): Self {
|
||||||
|
logger.debug { "Unexpected message: $message." }
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return this as Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinalServerState
|
@ -0,0 +1,18 @@
|
|||||||
|
package world.phantasmal.psoserv.servers
|
||||||
|
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import java.net.Socket
|
||||||
|
|
||||||
|
fun Socket.read(buffer: Buffer, size: Int): Int {
|
||||||
|
val read = getInputStream().read(buffer.byteArray, buffer.size, size)
|
||||||
|
|
||||||
|
if (read != -1) {
|
||||||
|
buffer.size += read
|
||||||
|
}
|
||||||
|
|
||||||
|
return read
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Socket.write(buffer: Buffer, offset: Int, size: Int) {
|
||||||
|
getOutputStream().write(buffer.byteArray, offset, size)
|
||||||
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
package world.phantasmal.psoserv.servers
|
||||||
|
|
||||||
|
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.messageString
|
||||||
|
import world.phantasmal.psoserv.roundToBlockSize
|
||||||
|
import java.net.Socket
|
||||||
|
import java.net.SocketException
|
||||||
|
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}"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var running = false
|
||||||
|
|
||||||
|
protected abstract val decryptCipher: Cipher?
|
||||||
|
protected abstract val encryptCipher: Cipher?
|
||||||
|
|
||||||
|
fun listen() {
|
||||||
|
logger.info { "Listening to $sockName." }
|
||||||
|
running = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
val readBuffer = Buffer.withCapacity(BUFFER_CAPACITY, Endianness.Little)
|
||||||
|
val headerBuffer = Buffer.withSize(headerSize, Endianness.Little)
|
||||||
|
|
||||||
|
readLoop@ while (true) {
|
||||||
|
// Read from socket.
|
||||||
|
val readSize = socket.read(readBuffer, BUFFER_CAPACITY - readBuffer.size)
|
||||||
|
|
||||||
|
if (readSize == -1) {
|
||||||
|
// Close the connection if no more bytes available.
|
||||||
|
break@readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process buffer contents.
|
||||||
|
var offset = 0
|
||||||
|
var bytesToSkip = 0
|
||||||
|
|
||||||
|
bufferLoop@ while (offset + headerSize <= readBuffer.size) {
|
||||||
|
// Remember the current cipher in a local variable because processing a message
|
||||||
|
// might change it.
|
||||||
|
val decryptCipher = decryptCipher
|
||||||
|
val encryptCipher = encryptCipher
|
||||||
|
|
||||||
|
// Read header.
|
||||||
|
readBuffer.copyInto(headerBuffer, offset = offset, size = headerSize)
|
||||||
|
|
||||||
|
// Decrypt header.
|
||||||
|
decryptCipher?.let {
|
||||||
|
check(decryptCipher.blockSize == headerSize)
|
||||||
|
decryptCipher.decrypt(headerBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (code, size) = 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.
|
||||||
|
encryptedSize > BUFFER_CAPACITY -> {
|
||||||
|
logger.warn {
|
||||||
|
val message = messageString(code, size)
|
||||||
|
"Receiving $message, too large: ${size}B. Skipping."
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesToSkip = encryptedSize - available
|
||||||
|
|
||||||
|
encryptCipher?.advance(
|
||||||
|
blocks = encryptedSize / encryptCipher.blockSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
break@bufferLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse message when we have enough bytes available.
|
||||||
|
available >= encryptedSize -> {
|
||||||
|
val messageBuffer = readBuffer.copy(offset, encryptedSize)
|
||||||
|
|
||||||
|
// Decrypt before parsing if necessary.
|
||||||
|
// Copy the already decrypted header first, then decrypt the rest. We
|
||||||
|
// don't simply decrypt the entire message buffer again, because the PC
|
||||||
|
// cipher is stateful.
|
||||||
|
headerBuffer.copyInto(messageBuffer)
|
||||||
|
decryptCipher?.decrypt(
|
||||||
|
messageBuffer,
|
||||||
|
offset = headerSize,
|
||||||
|
blocks = (encryptedSize - headerSize) / decryptCipher.blockSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val message = readMessage(messageBuffer)
|
||||||
|
logger.trace { "Received $message." }
|
||||||
|
|
||||||
|
when (processMessage(message)) {
|
||||||
|
ProcessResult.Ok -> {
|
||||||
|
// Advance the encryption cipher, then continue.
|
||||||
|
encryptCipher?.advance(
|
||||||
|
blocks = encryptedSize / encryptCipher.blockSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProcessResult.Changed -> {
|
||||||
|
// Copy changes to the read buffer and encrypt them if
|
||||||
|
// necessary.
|
||||||
|
messageBuffer.copyInto(
|
||||||
|
readBuffer,
|
||||||
|
destinationOffset = offset,
|
||||||
|
)
|
||||||
|
encryptCipher?.encrypt(
|
||||||
|
readBuffer,
|
||||||
|
offset,
|
||||||
|
blocks = encryptedSize / encryptCipher.blockSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProcessResult.Done -> {
|
||||||
|
// Close the connection.
|
||||||
|
break@readLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.error(e) { "Error while processing message from $sockName." }
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += encryptedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not enough bytes available.
|
||||||
|
else -> break@bufferLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processRawBytes(readBuffer, 0, readSize)
|
||||||
|
|
||||||
|
if (bytesToSkip > 0) {
|
||||||
|
// Just pass the raw bytes through to the raw bytes handler and don't parse
|
||||||
|
// them.
|
||||||
|
while (bytesToSkip > 0) {
|
||||||
|
readBuffer.size = 0
|
||||||
|
val read = socket.read(readBuffer, min(BUFFER_CAPACITY, bytesToSkip))
|
||||||
|
|
||||||
|
if (read == -1) {
|
||||||
|
logger.warn {
|
||||||
|
"Expected to skip $bytesToSkip more bytes, but $sockName stopped sending."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the connection.
|
||||||
|
break@readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesToSkip -= read
|
||||||
|
|
||||||
|
processRawBytes(readBuffer, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
readBuffer.size = 0
|
||||||
|
} else {
|
||||||
|
// If we didn't have enough bytes available, shift the unparsed bytes to the
|
||||||
|
// front of the buffer before we read more bytes. If we don't do this, we can
|
||||||
|
// end up in an infinite loop.
|
||||||
|
val unparsed = readBuffer.size - offset
|
||||||
|
|
||||||
|
if (unparsed > 0) {
|
||||||
|
readBuffer.copyInto(readBuffer, offset = offset, size = unparsed)
|
||||||
|
readBuffer.size = unparsed
|
||||||
|
} else {
|
||||||
|
readBuffer.size = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
logger.error(e) { "Interrupted while listening to $sockName, closing connection." }
|
||||||
|
} 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) { "Error while listening to $sockName, closing connection." }
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.error(e) { "Error while listening to $sockName, closing connection." }
|
||||||
|
} finally {
|
||||||
|
running = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (socket.isClosed) {
|
||||||
|
logger.info { "$sockName was closed." }
|
||||||
|
} else {
|
||||||
|
logger.info { "Closing connection to $sockName." }
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
socketClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeBytes(buffer: Buffer, offset: Int, size: Int) {
|
||||||
|
socket.write(buffer, offset, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
running = false
|
||||||
|
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) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun socketClosed() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum class ProcessResult {
|
||||||
|
Ok, Changed, Done
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BUFFER_CAPACITY: Int = 32768
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.character
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import world.phantasmal.psoserv.messages.BbMessage
|
||||||
|
import world.phantasmal.psoserv.servers.BbServer
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
class DataServer(address: InetAddress, port: Int) :
|
||||||
|
BbServer<DataState>(KotlinLogging.logger {}, address, port) {
|
||||||
|
|
||||||
|
override fun initializeState(sender: ClientSender): DataState {
|
||||||
|
val ctx = DataContext(sender)
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.InitEncryption(
|
||||||
|
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||||
|
sender.serverCipher.key,
|
||||||
|
sender.clientCipher.key,
|
||||||
|
),
|
||||||
|
encrypt = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
return DataState.Authentication(ctx)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.character
|
||||||
|
|
||||||
|
import world.phantasmal.core.math.clamp
|
||||||
|
import world.phantasmal.psolib.buffer.Buffer
|
||||||
|
import world.phantasmal.psolib.cursor.cursor
|
||||||
|
import world.phantasmal.psoserv.messages.BbAuthenticationStatus
|
||||||
|
import world.phantasmal.psoserv.messages.BbMessage
|
||||||
|
import world.phantasmal.psoserv.messages.PsoCharacter
|
||||||
|
import world.phantasmal.psoserv.servers.FinalServerState
|
||||||
|
import world.phantasmal.psoserv.servers.Server
|
||||||
|
import world.phantasmal.psoserv.servers.ServerState
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class DataContext(
|
||||||
|
private val sender: Server.ClientSender,
|
||||||
|
) {
|
||||||
|
fun send(message: BbMessage, encrypt: Boolean = true) {
|
||||||
|
sender.send(message, encrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DataState : ServerState<BbMessage, DataState>() {
|
||||||
|
class Authentication(private val ctx: DataContext) : DataState() {
|
||||||
|
override fun process(message: BbMessage): DataState =
|
||||||
|
if (message is BbMessage.Authenticate) {
|
||||||
|
// TODO: Actual authentication.
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.AuthenticationResponse(
|
||||||
|
BbAuthenticationStatus.Success,
|
||||||
|
message.guildCard,
|
||||||
|
message.teamId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Account(ctx)
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Account(private val ctx: DataContext) : DataState() {
|
||||||
|
override fun process(message: BbMessage): DataState =
|
||||||
|
if (message is BbMessage.GetAccount) {
|
||||||
|
// TODO: Send correct guild card number and team ID.
|
||||||
|
ctx.send(BbMessage.Account(0, 0))
|
||||||
|
|
||||||
|
CharacterSelect(ctx)
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CharacterSelect(private val ctx: DataContext) : DataState() {
|
||||||
|
override fun process(message: BbMessage): DataState =
|
||||||
|
when (message) {
|
||||||
|
is BbMessage.CharacterSelect -> {
|
||||||
|
// TODO: Look up character data.
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.CharacterSelectResponse(
|
||||||
|
PsoCharacter(
|
||||||
|
slot = message.slot,
|
||||||
|
exp = 0,
|
||||||
|
level = 1,
|
||||||
|
guildCardString = "",
|
||||||
|
nameColor = 0,
|
||||||
|
model = 0,
|
||||||
|
nameColorChecksum = 0,
|
||||||
|
sectionId = 0,
|
||||||
|
characterClass = 0,
|
||||||
|
costume = 0,
|
||||||
|
skin = 0,
|
||||||
|
face = 0,
|
||||||
|
head = 0,
|
||||||
|
hair = 0,
|
||||||
|
hairRed = 0,
|
||||||
|
hairGreen = 0,
|
||||||
|
hairBlue = 0,
|
||||||
|
propX = 1.0,
|
||||||
|
propY = 1.0,
|
||||||
|
name = "Phantasmal ${message.slot}",
|
||||||
|
playTime = 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.Checksum -> {
|
||||||
|
// TODO: Checksum checking.
|
||||||
|
ctx.send(BbMessage.ChecksumResponse(true))
|
||||||
|
|
||||||
|
DataDownload(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataDownload(private val ctx: DataContext) : DataState() {
|
||||||
|
private val guildCardBuffer = Buffer.withSize(54672)
|
||||||
|
private val fileBuffer = Buffer.withSize(0)
|
||||||
|
private var fileChunkNo = 0
|
||||||
|
|
||||||
|
override fun process(message: BbMessage): DataState =
|
||||||
|
when (message) {
|
||||||
|
is BbMessage.GetGuildCardHeader -> {
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.GuildCardHeader(
|
||||||
|
guildCardBuffer.size,
|
||||||
|
crc32(guildCardBuffer),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.GetGuildCardChunk -> {
|
||||||
|
if (message.cont) {
|
||||||
|
val offset =
|
||||||
|
clamp(message.chunkNo * MAX_CHUNK_SIZE, 0, guildCardBuffer.size)
|
||||||
|
val size = min(guildCardBuffer.size - offset, MAX_CHUNK_SIZE)
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.GuildCardChunk(
|
||||||
|
message.chunkNo,
|
||||||
|
guildCardBuffer.cursor(offset, size),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.GetFileList -> {
|
||||||
|
ctx.send(BbMessage.FileList())
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is BbMessage.GetFileChunk -> {
|
||||||
|
val offset = min(fileChunkNo * MAX_CHUNK_SIZE, fileBuffer.size)
|
||||||
|
val size = min(fileBuffer.size - offset, MAX_CHUNK_SIZE)
|
||||||
|
|
||||||
|
ctx.send(BbMessage.FileChunk(fileChunkNo, fileBuffer.cursor(offset, size)))
|
||||||
|
|
||||||
|
if (offset + size < fileBuffer.size) {
|
||||||
|
fileChunkNo++
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc32(data: Buffer): Int {
|
||||||
|
val cursor = data.cursor()
|
||||||
|
var cs = 0xFFFFFFFFu
|
||||||
|
|
||||||
|
while (cursor.hasBytesLeft()) {
|
||||||
|
cs = cs xor cursor.uByte().toUInt()
|
||||||
|
|
||||||
|
for (i in 0..7) {
|
||||||
|
cs = if ((cs and 1u) == 0u) {
|
||||||
|
cs shr 1
|
||||||
|
} else {
|
||||||
|
(cs shr 1) xor 0xEDB88320u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cs xor 0xFFFFFFFFu).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_CHUNK_SIZE: Int = 0x6800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Final : DataState(), FinalServerState {
|
||||||
|
override fun process(message: BbMessage): DataState =
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.login
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import world.phantasmal.psoserv.messages.BbMessage
|
||||||
|
import world.phantasmal.psoserv.servers.BbServer
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
class LoginServer(
|
||||||
|
address: InetAddress,
|
||||||
|
port: Int,
|
||||||
|
private val characterServerAddress: Inet4Address,
|
||||||
|
private val characterServerPort: Int,
|
||||||
|
) : BbServer<LoginState>(KotlinLogging.logger {}, address, port) {
|
||||||
|
|
||||||
|
override fun initializeState(sender: ClientSender): LoginState {
|
||||||
|
val ctx = LoginContext(sender, characterServerAddress.address, characterServerPort)
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.InitEncryption(
|
||||||
|
"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.",
|
||||||
|
sender.serverCipher.key,
|
||||||
|
sender.clientCipher.key,
|
||||||
|
),
|
||||||
|
encrypt = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
return LoginState.Authentication(ctx)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.login
|
||||||
|
|
||||||
|
import world.phantasmal.psoserv.messages.BbAuthenticationStatus
|
||||||
|
import world.phantasmal.psoserv.messages.BbMessage
|
||||||
|
import world.phantasmal.psoserv.servers.FinalServerState
|
||||||
|
import world.phantasmal.psoserv.servers.Server
|
||||||
|
import world.phantasmal.psoserv.servers.ServerState
|
||||||
|
|
||||||
|
class LoginContext(
|
||||||
|
private val sender: Server.ClientSender,
|
||||||
|
val characterServerAddress: ByteArray,
|
||||||
|
val characterServerPort: Int,
|
||||||
|
) {
|
||||||
|
fun send(message: BbMessage, encrypt: Boolean = true) {
|
||||||
|
sender.send(message, encrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LoginState : ServerState<BbMessage, LoginState>() {
|
||||||
|
class Authentication(private val ctx: LoginContext) : LoginState() {
|
||||||
|
override fun process(message: BbMessage): LoginState =
|
||||||
|
if (message is BbMessage.Authenticate) {
|
||||||
|
// TODO: Actual authentication.
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.AuthenticationResponse(
|
||||||
|
BbAuthenticationStatus.Success,
|
||||||
|
message.guildCard,
|
||||||
|
message.teamId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ctx.send(
|
||||||
|
BbMessage.Redirect(
|
||||||
|
ctx.characterServerAddress,
|
||||||
|
ctx.characterServerPort,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Final
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Final : LoginState(), FinalServerState {
|
||||||
|
override fun process(message: BbMessage): LoginState =
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.patch
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
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.servers.Server
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
class PatchServer(address: InetAddress, port: Int, private val welcomeMessage: String) :
|
||||||
|
Server<PcMessage, PatchState>(KotlinLogging.logger {}, address, port) {
|
||||||
|
|
||||||
|
override fun createCipher() = PcCipher()
|
||||||
|
|
||||||
|
override fun initializeState(sender: ClientSender): PatchState {
|
||||||
|
val ctx = PatchContext(sender, welcomeMessage)
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
PcMessage.InitEncryption(
|
||||||
|
"Patch Server. Copyright SonicTeam, LTD. 2001",
|
||||||
|
sender.serverCipher.key,
|
||||||
|
sender.clientCipher.key,
|
||||||
|
),
|
||||||
|
encrypt = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
return PatchState.Welcome(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readHeader(buffer: Buffer): Header =
|
||||||
|
PcMessage.readHeader(buffer)
|
||||||
|
|
||||||
|
override fun readMessage(buffer: Buffer): PcMessage =
|
||||||
|
PcMessage.fromBuffer(buffer)
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package world.phantasmal.psoserv.servers.patch
|
||||||
|
|
||||||
|
import world.phantasmal.psoserv.messages.PcMessage
|
||||||
|
import world.phantasmal.psoserv.servers.FinalServerState
|
||||||
|
import world.phantasmal.psoserv.servers.Server
|
||||||
|
import world.phantasmal.psoserv.servers.ServerState
|
||||||
|
|
||||||
|
class PatchContext(
|
||||||
|
private val sender: Server.ClientSender,
|
||||||
|
val welcomeMessage: String,
|
||||||
|
) {
|
||||||
|
fun send(message: PcMessage, encrypt: Boolean = true) {
|
||||||
|
sender.send(message, encrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PatchState : ServerState<PcMessage, PatchState>() {
|
||||||
|
class Welcome(private val ctx: PatchContext) : PatchState() {
|
||||||
|
override fun process(message: PcMessage): PatchState =
|
||||||
|
if (message is PcMessage.InitEncryption) {
|
||||||
|
ctx.send(PcMessage.Login())
|
||||||
|
|
||||||
|
Login(ctx)
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Login(private val ctx: PatchContext) : PatchState() {
|
||||||
|
override fun process(message: PcMessage): PatchState =
|
||||||
|
if (message is PcMessage.Login) {
|
||||||
|
ctx.send(PcMessage.WelcomeMessage(ctx.welcomeMessage))
|
||||||
|
ctx.send(PcMessage.PatchListStart())
|
||||||
|
ctx.send(PcMessage.PatchListEnd())
|
||||||
|
|
||||||
|
PatchListDone(ctx)
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchListDone(private val ctx: PatchContext) : PatchState() {
|
||||||
|
override fun process(message: PcMessage): PatchState =
|
||||||
|
if (message is PcMessage.PatchListOk) {
|
||||||
|
ctx.send(PcMessage.PatchDone())
|
||||||
|
|
||||||
|
Final
|
||||||
|
} else {
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Final : PatchState(), FinalServerState {
|
||||||
|
override fun process(message: PcMessage): PatchState =
|
||||||
|
unexpectedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
13
psoserv/src/main/resources/log4j2.xml
Normal file
13
psoserv/src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="info">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
@ -4,6 +4,7 @@ include(
|
|||||||
":core",
|
":core",
|
||||||
":psolib",
|
":psolib",
|
||||||
":observable",
|
":observable",
|
||||||
|
":psoserv",
|
||||||
":test-utils",
|
":test-utils",
|
||||||
":web",
|
":web",
|
||||||
":web:assembly-worker",
|
":web:assembly-worker",
|
||||||
|
Loading…
Reference in New Issue
Block a user