Changed forwarder to use websockets and improve stability/reusability of connection

This commit is contained in:
Josh-Larson
2022-07-05 00:38:35 -05:00
parent 99b81aa05f
commit f892d4beea
12 changed files with 350 additions and 167 deletions

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "client-holocore"]
path = client-holocore
url = https://github.com/ProjectSWGCore/client-holocore.git
[submodule "pswgcommon"]
path = pswgcommon
url = https://github.com/ProjectSWGCore/pswgcommon.git

View File

@@ -20,17 +20,17 @@ idea {
}
repositories {
mavenLocal()
jcenter()
maven("https://dev.joshlarson.me/maven2")
mavenCentral()
}
sourceSets {
main {
java.outputDir = File(java.outputDir.toString().replace("\\${File.separatorChar}java", ""))
dependencies {
implementation(group="org.jetbrains", name="annotations", version="20.1.0")
implementation(project(":pswgcommon"))
implementation(project(":client-holocore"))
api(group="me.joshlarson", name="jlcommon-network", version="1.1.1")
implementation(group="me.joshlarson", name="websocket", version="0.9.3")
}
}
test {
@@ -45,5 +45,5 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach
kotlinOptions {
jvmTarget = kotlinTargetJdk
}
destinationDir = sourceSets.main.get().java.outputDir
destinationDirectory.set(File(destinationDirectory.get().asFile.path.replace("kotlin", "java")))
}

Submodule client-holocore deleted from 42894ee7e1

View File

@@ -1 +1 @@
include(":pswgcommon", ":client-holocore")
include(":pswgcommon")

View File

@@ -4,15 +4,12 @@ import com.projectswg.forwarder.intents.*
import me.joshlarson.jlcommon.concurrency.Delay
import me.joshlarson.jlcommon.control.IntentManager
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.SafeMain
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.jlcommon.log.Log.LogLevel
import me.joshlarson.jlcommon.log.log_wrapper.ConsoleLogWrapper
import me.joshlarson.jlcommon.utilities.ThreadUtilities
import java.io.*
import java.net.InetSocketAddress
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.ZipEntry
@@ -109,11 +106,11 @@ class Forwarder {
class ForwarderData internal constructor() {
var address: InetSocketAddress? = null
var isVerifyServer = true
var isEncryptionEnabled = true
var baseConnectionUri: String? = null
var username: String? = null
var password: String? = null
var protocolVersion: String? = null
var loginPort = 0
var zonePort = 0
var pingPort = 0
@@ -121,22 +118,15 @@ class Forwarder {
var outboundTunerInterval = 20
var crashed: Boolean = false
}
companion object {
val connectionUri: URI
get() {
val encodedUsername = Base64.getEncoder().encodeToString((username ?: "").encodeToByteArray())
val encodedPassword = Base64.getEncoder().encodeToString((password ?: "").encodeToByteArray())
val encodedProtocolVersion = Base64.getEncoder().encodeToString((protocolVersion ?: "").encodeToByteArray())
val connectionUriStr = "$baseConnectionUri?username=$encodedUsername&password=$encodedPassword&protocolVersion=$encodedProtocolVersion"
return URI(connectionUriStr)
}
@JvmStatic
fun main(args: Array<String>) {
SafeMain.main("") { mainRunnable() }
}
private fun mainRunnable() {
Log.addWrapper(ConsoleLogWrapper(LogLevel.TRACE))
val forwarder = Forwarder()
forwarder.data.address = InetSocketAddress(44463)
forwarder.run()
ThreadUtilities.printActiveThreads()
}
}
}

View File

@@ -2,34 +2,13 @@ package com.projectswg.forwarder.resources.networking
import com.projectswg.common.network.NetBuffer
import com.projectswg.common.network.packets.PacketType
import com.projectswg.common.network.packets.swg.holo.login.HoloLoginRequestPacket
import com.projectswg.common.network.packets.swg.login.LoginClientId
import com.projectswg.common.network.packets.swg.login.LoginClusterStatus
import com.projectswg.forwarder.Forwarder.ForwarderData
import com.projectswg.holocore.client.HolocoreSocket
import java.nio.ByteBuffer
import java.nio.ByteOrder
class NetInterceptor(private val data: ForwarderData) {
fun interceptClient(holocore: HolocoreSocket, data: ByteArray) {
if (data.size < 6) {
holocore.send(data)
return
}
val bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
when (PacketType.fromCrc(bb.getInt(2))) {
PacketType.LOGIN_CLIENT_ID -> {
val loginClientId = LoginClientId(NetBuffer.wrap(bb))
if (loginClientId.username == this.data.username && loginClientId.password.isEmpty())
loginClientId.password = this.data.password
holocore.send(HoloLoginRequestPacket(loginClientId.username, loginClientId.password).encode().array())
}
else -> holocore.send(data)
}
}
fun interceptServer(data: ByteArray): ByteArray {
if (data.size < 6)
return data

View File

@@ -0,0 +1,306 @@
package com.projectswg.forwarder.resources.server
import com.projectswg.common.network.NetBuffer
import com.projectswg.common.network.packets.PacketType
import com.projectswg.common.network.packets.swg.holo.HoloConnectionStarted
import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped
import com.projectswg.common.network.packets.swg.holo.login.HoloLoginResponsePacket
import com.projectswg.common.network.packets.swg.login.*
import com.projectswg.forwarder.Forwarder
import com.projectswg.forwarder.intents.DataPacketOutboundIntent
import com.projectswg.forwarder.intents.ServerConnectedIntent
import com.projectswg.forwarder.intents.ServerDisconnectedIntent
import com.projectswg.forwarder.resources.networking.NetInterceptor
import me.joshlarson.jlcommon.concurrency.Delay
import me.joshlarson.jlcommon.control.IntentChain
import me.joshlarson.jlcommon.control.IntentManager
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.websocket.client.WebSocketClientCallback
import me.joshlarson.websocket.client.WebSocketClientProtocol
import me.joshlarson.websocket.common.WebSocketHandler
import me.joshlarson.websocket.common.parser.http.HttpResponse
import me.joshlarson.websocket.common.parser.websocket.WebSocketCloseReason
import me.joshlarson.websocket.common.parser.websocket.WebsocketFrame
import me.joshlarson.websocket.common.parser.websocket.WebsocketFrameType
import java.io.Closeable
import java.io.IOException
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
import java.net.URI
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import kotlin.concurrent.withLock
class HolocoreConnection(private val intentManager: IntentManager,
private val interceptor: NetInterceptor,
private val forwarderData: Forwarder.ForwarderData) : Closeable {
private val intentChain: IntentChain = IntentChain()
private val upgradeDelaySemaphore = Semaphore(0)
private val outputStreamLock = ReentrantLock()
private val connectionStatus = AtomicReference(ServerConnectionStatus.DISCONNECTED)
private val disconnectReason = AtomicReference(HoloConnectionStopped.ConnectionStoppedReason.UNKNOWN)
private val connectionSender = AtomicReference<((ByteArray) -> Unit)?>(null)
private val initialConnectionUri = forwarderData.connectionUri
private val socket: Socket = createSocket(initialConnectionUri)
private val port: Int = when {
initialConnectionUri.port != -1 -> initialConnectionUri.port
initialConnectionUri.scheme == "wss" -> 443
initialConnectionUri.scheme == "ws" -> 80
else -> {
Log.e("Undefined port in connection URI")
throw IllegalArgumentException("initialConnectionUri")
}
}
private lateinit var connectionUri: URI
private lateinit var outputStream: OutputStream
private lateinit var wsProtocol: WebSocketClientProtocol
override fun close() {
socket.close()
}
fun getConnectionStatus(): ServerConnectionStatus {
return connectionStatus.get()
}
fun setDisconnectReason(reason: HoloConnectionStopped.ConnectionStoppedReason) {
this.disconnectReason.set(reason)
}
fun sendPacket(packet: ByteArray) {
val bb = ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN)
when (PacketType.fromCrc(bb.getInt(2))) {
PacketType.LOGIN_CLIENT_ID -> {
val loginClientId = LoginClientId(NetBuffer.wrap(bb))
if (loginClientId.username == this.forwarderData.username && loginClientId.password.isEmpty())
loginClientId.password = this.forwarderData.password
Log.i("Received login packet for %s", loginClientId.username)
forwarderData.username = loginClientId.username
forwarderData.password = loginClientId.password
forwarderData.protocolVersion = PROTOCOL_VERSION
upgradeDelaySemaphore.drainPermits()
upgradeDelaySemaphore.release()
connectionSender.get()?.invoke(loginClientId.encode().array())
}
else -> {
connectionSender.get()?.invoke(packet)
}
}
}
fun handle() {
if (!connectAndUpgrade())
return
handleOnConnect()
try {
handleReadLoop()
startGracefulDisconnect()
} finally {
handleOnDisconnect()
}
}
private fun connectAndUpgrade(): Boolean {
Log.t("Attempting to connect to server at %s://%s:%d", initialConnectionUri.scheme, initialConnectionUri.host, port)
try {
socket.connect(InetSocketAddress(initialConnectionUri.host, port), CONNECT_TIMEOUT)
} catch (e: IOException) {
Log.w("Failed to connect to server: %s", e.message)
return false
} catch (e: InterruptedException) {
Log.w("Server connection timed out")
return false
}
intentChain.broadcastAfter(intentManager, ServerConnectedIntent())
Log.t("Connected to server via TCP - awaiting login packet...")
upgradeDelaySemaphore.drainPermits() // Ensure we have exactly zero permits
try {
upgradeDelaySemaphore.tryAcquire(30, TimeUnit.SECONDS)
} catch (e: InterruptedException) {
Log.w("No login packet received within 30s - terminating connection")
return false
}
Log.t("Received login packet - upgrading to websocket")
connectionUri = forwarderData.connectionUri
outputStream = socket.getOutputStream()
wsProtocol = WebSocketClientProtocol(WebsocketHandler(), connectionUri.rawPath + "?" + connectionUri.rawQuery, ::writeToOutputStream, socket::close)
return true
}
private fun handleOnConnect() {
wsProtocol.onConnect()
}
private fun handleReadLoop() {
val inputStream = socket.getInputStream()
val buffer = ByteArray(4096)
val startOfConnection = System.nanoTime()
while (!Delay.isInterrupted()) {
val n = inputStream.read(buffer)
if (n <= 0)
break
wsProtocol.onRead(buffer, 0, n)
if (connectionStatus.get() == ServerConnectionStatus.CONNECTING && System.nanoTime() - startOfConnection >= CONNECT_TIMEOUT) {
Log.e("Failed to connect to server")
return
}
}
}
private fun startGracefulDisconnect() {
val inputStream = socket.getInputStream()
val buffer = ByteArray(4096)
connectionStatus.set(ServerConnectionStatus.DISCONNECTING)
wsProtocol.send(WebsocketFrame(WebsocketFrameType.BINARY, HoloConnectionStopped(disconnectReason.get()).encode().array()))
wsProtocol.sendClose()
val startOfDisconnect = System.nanoTime()
socket.soTimeout = 3000
while (connectionStatus.get() != ServerConnectionStatus.DISCONNECTED && System.nanoTime() - startOfDisconnect < 5e9) {
val n = inputStream.read(buffer)
if (n <= 0)
break
wsProtocol.onRead(buffer, 0, n)
}
}
private fun handlePacket(data: ByteArray) {
val bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
when (PacketType.fromCrc(bb.getInt(2))) {
PacketType.LOGIN_CLUSTER_STATUS -> {
val cluster = LoginClusterStatus(NetBuffer.wrap(data))
for (g in cluster.galaxies) {
g.address = "127.0.0.1"
g.zonePort = this.forwarderData.zonePort
g.pingPort = this.forwarderData.pingPort
}
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(cluster.encode().array()))
}
PacketType.HOLO_LOGIN_RESPONSE -> {
val response = HoloLoginResponsePacket(NetBuffer.wrap(data))
for (g in response.galaxies) {
g.address = "127.0.0.1"
g.zonePort = this.forwarderData.zonePort
g.pingPort = this.forwarderData.pingPort
}
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginClientToken(ByteArray(24), 0, "").encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(CharacterCreationDisabled().encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginEnumCluster(response.galaxies, 2).encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(EnumerateCharacterId(response.characters).encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginClusterStatus(response.galaxies).encode().array()))
}
else -> {
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(interceptor.interceptServer(data)))
}
}
}
private fun handleOnDisconnect() {
wsProtocol.onDisconnect()
if (connectionStatus.get() != ServerConnectionStatus.DISCONNECTED) {
Log.w("Server connection interrupted")
} else {
Log.i("Successfully closed server connection")
}
intentChain.broadcastAfter(intentManager, ServerDisconnectedIntent())
}
private fun writeToOutputStream(data: ByteArray) {
outputStreamLock.withLock {
outputStream.write(data)
}
}
private inner class WebsocketHandler : WebSocketClientCallback {
override fun onUpgrade(obj: WebSocketHandler, response: HttpResponse) {
connectionSender.set(obj::sendBinary)
}
override fun onDisconnect(obj: WebSocketHandler, closeCode: Int, reason: String) {
connectionStatus.set(ServerConnectionStatus.DISCONNECTED)
}
override fun onBinaryMessage(obj: WebSocketHandler, rawData: ByteArray) {
val dataBuffer = NetBuffer.wrap(rawData)
dataBuffer.short
when (dataBuffer.int) {
HoloConnectionStarted.CRC -> {
Log.i("Successfully connected to server at %s", connectionUri.toASCIIString())
connectionStatus.set(ServerConnectionStatus.CONNECTED)
}
HoloConnectionStopped.CRC -> {
val packet = HoloConnectionStopped()
packet.decode(NetBuffer.wrap(rawData))
if (connectionStatus.get() != ServerConnectionStatus.DISCONNECTING) {
connectionStatus.set(ServerConnectionStatus.DISCONNECTING)
obj.sendBinary(HoloConnectionStopped(packet.reason).encode().array())
}
obj.close(WebSocketCloseReason.NORMAL.statusCode.toInt(), packet.reason.name)
}
else -> {
handlePacket(rawData)
}
}
}
}
enum class ServerConnectionStatus {
CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED
}
companion object {
private const val PROTOCOL_VERSION = "20220620-15:00"
private const val CONNECT_TIMEOUT = 5000
private fun createSocket(connectionUri: URI): Socket {
if (connectionUri.scheme == "wss") {
val sslContext = SSLContext.getInstance("TLSv1.3")
val tm = null // To disable server verification, use: arrayOf<TrustManager>(TrustingTrustManager())
sslContext.init(null, tm, SecureRandom())
val sslSocket = sslContext.socketFactory.createSocket() as SSLSocket
sslSocket.enabledProtocols = arrayOf("TLSv1.3")
// Last Updated: 02 May 2021
sslSocket.enabledCipherSuites = sslSocket.supportedCipherSuites
// We want either AES256 GCM or CHACHA20, in the TLSv1.3 cipher format
.filter {it.startsWith("TLS_AES_256_GCM") || it.startsWith("TLS_CHACHA20")}
// SHA256 and SHA384 are both solid hashing algorithms
.filter {it.endsWith("SHA256") || it.endsWith("SHA384")}
// Prioritize CHACHA20, because it is stream-based rather than block-based
.sortedBy { if (it.startsWith("TLS_CHACHA20")) 0 else 1 }
.toTypedArray()
Log.t("Using TLSv1.3 ciphers: %s", Arrays.toString(sslSocket.enabledCipherSuites))
return sslSocket
} else {
return Socket()
}
}
}
}

View File

@@ -32,8 +32,8 @@ class ClientOutboundDataService : Service() {
private val outboundBuffer: Array<SequencedOutbound?> = arrayOfNulls(4096)
private val multiplexer: IntentMultiplexer = IntentMultiplexer(this, ProtocolStack::class.java, Packet::class.java)
private val activeStacks: MutableSet<ProtocolStack> = ConcurrentHashMap.newKeySet()
private val sendThread: BasicThread = BasicThread("outbound-sender", Runnable { this.persistentSend() })
private val heartbeatThread: BasicScheduledThread = BasicScheduledThread("heartbeat", Runnable { this.heartbeat() })
private val sendThread: BasicThread = BasicThread("outbound-sender") { this.persistentSend() }
private val heartbeatThread: BasicScheduledThread = BasicScheduledThread("heartbeat") { this.heartbeat() }
private val zoningIn: AtomicBoolean = AtomicBoolean(false)
private val packetNotifyLock: ReentrantLock = ReentrantLock()
private val packetNotify: Condition = packetNotifyLock.newCondition()

View File

@@ -18,7 +18,6 @@ import java.nio.BufferUnderflowException
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Consumer
class ClientServerService : Service() {
@@ -57,11 +56,11 @@ class ClientServerService : Service() {
try {
val data = sfi.data
Log.t("Initializing login udp server...")
loginServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.loginPort), 16384, Consumer<DatagramPacket> { this.onLoginPacket(it) })
loginServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.loginPort), 16384) { this.onLoginPacket(it) }
Log.t("Initializing zone udp server...")
zoneServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.zonePort), 16384, Consumer<DatagramPacket> { this.onZonePacket(it) })
zoneServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.zonePort), 16384) { this.onZonePacket(it) }
Log.t("Initializing ping udp server...")
pingServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.pingPort), 16384, Consumer<DatagramPacket> { this.onPingPacket(it) })
pingServer = UDPServer(InetSocketAddress(InetAddress.getLoopbackAddress(), data.pingPort), 16384) { this.onPingPacket(it) }
Log.t("Binding to login server...")
loginServer.bind { this.customizeUdpServer(it) }

View File

@@ -56,7 +56,7 @@ class IntentRecordingService : Service() {
if (data == null)
log(cci, "")
else
log(cci, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.address, data.username, data.loginPort, data.zonePort)
log(cci, "Base URL='%s' Username='%s' Login='%d' Zone='%d'", data.baseConnectionUri, data.username, data.loginPort, data.zonePort)
}
@IntentHandler
@@ -75,7 +75,7 @@ class IntentRecordingService : Service() {
if (data == null)
log(sfi, "")
else
log(sfi, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.address, data.username, data.loginPort, data.zonePort)
log(sfi, "Base URL='%s' Username='%s' Login='%d' Zone='%d'", data.baseConnectionUri, data.username, data.loginPort, data.zonePort)
}
@IntentHandler
@@ -84,7 +84,7 @@ class IntentRecordingService : Service() {
if (data == null)
log(sci, "")
else
log(sci, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.address, data.username, data.loginPort, data.zonePort)
log(sci, "Base URL='%s' Username='%s' Login='%d' Zone='%d'", data.baseConnectionUri, data.username, data.loginPort, data.zonePort)
}
@IntentHandler

View File

@@ -1,9 +1,9 @@
module com.projectswg.forwarder {
requires com.projectswg.common;
requires com.projectswg.holocore.client;
requires org.jetbrains.annotations;
requires me.joshlarson.jlcommon;
requires me.joshlarson.jlcommon.network;
requires me.joshlarson.websocket;
requires java.management;
requires kotlin.stdlib;

View File

@@ -1,31 +1,24 @@
package com.projectswg.forwarder.services.server
import com.projectswg.common.network.NetBuffer
import com.projectswg.common.network.packets.PacketType
import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped
import com.projectswg.common.network.packets.swg.holo.login.HoloLoginResponsePacket
import com.projectswg.common.network.packets.swg.login.*
import com.projectswg.forwarder.Forwarder.ForwarderData
import com.projectswg.forwarder.intents.*
import com.projectswg.forwarder.resources.networking.NetInterceptor
import com.projectswg.holocore.client.HolocoreSocket
import com.projectswg.forwarder.resources.server.HolocoreConnection
import me.joshlarson.jlcommon.concurrency.BasicThread
import me.joshlarson.jlcommon.control.IntentChain
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.atomic.AtomicReference
class ServerConnectionService : Service() {
private val intentChain: IntentChain = IntentChain()
private val thread: BasicThread = BasicThread("server-connection", Runnable { this.primaryConnectionLoop() })
private val thread: BasicThread = BasicThread("server-connection") { this.primaryConnectionLoop() }
private val currentConnection = AtomicReference<HolocoreConnection?>(null)
private lateinit var interceptor: NetInterceptor // set by StartForwarderIntent
private lateinit var data: ForwarderData // set by StartForwarderIntent
private var holocore: HolocoreSocket? = null
override fun stop(): Boolean {
return stopRunningLoop(HoloConnectionStopped.ConnectionStoppedReason.APPLICATION)
@@ -44,8 +37,7 @@ class ServerConnectionService : Service() {
@IntentHandler
private fun handleRequestServerConnectionIntent(rsci: RequestServerConnectionIntent) {
val holocore = this.holocore
if (holocore != null)
if (currentConnection.get()?.getConnectionStatus() == HolocoreConnection.ServerConnectionStatus.CONNECTING)
return // It's trying to connect - give it a little more time
if (stopRunningLoop(HoloConnectionStopped.ConnectionStoppedReason.NEW_CONNECTION))
@@ -59,114 +51,35 @@ class ServerConnectionService : Service() {
@IntentHandler
private fun handleDataPacketInboundIntent(dpii: DataPacketInboundIntent) {
val holocore = this.holocore ?: return
val data = dpii.data
if (data.size < 6) {
return // not a valid packet
}
val bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
when (PacketType.fromCrc(bb.getInt(2))) {
PacketType.LOGIN_CLIENT_ID -> {
val loginClientId = LoginClientId(NetBuffer.wrap(bb))
if (loginClientId.username == this.data.username && loginClientId.password.isEmpty())
loginClientId.password = this.data.password
holocore.send(loginClientId.encode().array())
}
else -> holocore.send(data)
}
currentConnection.get()?.sendPacket(data)
}
private fun stopRunningLoop(reason: HoloConnectionStopped.ConnectionStoppedReason): Boolean {
if (!thread.isExecuting)
return true
thread.stop(true)
currentConnection.get()?.setDisconnectReason(reason)
Log.d("Terminating connection with the server. Reason: $reason")
holocore?.send(HoloConnectionStopped(reason).encode().array())
holocore?.close()
return thread.awaitTermination(1000)
thread.stop(true)
return thread.awaitTermination(5000)
}
private fun primaryConnectionLoop() {
var didConnect = false
try {
val address = data.address ?: return
HolocoreSocket(address.address, address.port, data.isVerifyServer, data.isEncryptionEnabled).use { holocore ->
this.holocore = holocore
Log.t("Attempting to connect to server at %s", holocore.remoteAddress)
try {
holocore.connect(CONNECT_TIMEOUT)
} catch (e: IOException) {
Log.e("Failed to connect to server")
return
}
didConnect = true
intentChain.broadcastAfter(intentManager, ServerConnectedIntent())
Log.i("Successfully connected to server at %s", holocore.remoteAddress)
while (holocore.isConnected) {
if (primaryConnectionLoopReceive(holocore))
continue // More packets to receive
if (holocore.isConnected)
Log.w("Server connection interrupted")
else
Log.w("Server closed connection!")
return
}
HolocoreConnection(intentManager, interceptor, data).use {
currentConnection.set(it)
it.handle()
currentConnection.set(null)
}
} catch (t: Throwable) {
Log.w("Caught unknown exception in server connection! %s: %s", t.javaClass.name, t.message)
Log.w(t)
} finally {
Log.i("Disconnected from server.")
if (didConnect)
intentChain.broadcastAfter(intentManager, ServerDisconnectedIntent())
this.holocore = null
}
}
private fun primaryConnectionLoopReceive(holocore: HolocoreSocket): Boolean {
val inbound = holocore.receive() ?: return false
val data = inbound.data
if (data.size < 6)
return true
val bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
when (PacketType.fromCrc(bb.getInt(2))) {
PacketType.LOGIN_CLUSTER_STATUS -> {
val cluster = LoginClusterStatus(NetBuffer.wrap(data))
for (g in cluster.galaxies) {
g.address = "127.0.0.1"
g.zonePort = this.data.zonePort
g.pingPort = this.data.pingPort
}
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(cluster.encode().array()))
}
PacketType.HOLO_LOGIN_RESPONSE -> {
val response = HoloLoginResponsePacket(NetBuffer.wrap(data))
for (g in response.galaxies) {
g.address = "127.0.0.1"
g.zonePort = this.data.zonePort
g.pingPort = this.data.pingPort
}
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginClientToken(ByteArray(24), 0, "").encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(CharacterCreationDisabled().encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginEnumCluster(response.galaxies, 2).encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(EnumerateCharacterId(response.characters).encode().array()))
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(LoginClusterStatus(response.galaxies).encode().array()))
}
else -> {
intentChain.broadcastAfter(intentManager, DataPacketOutboundIntent(interceptor.interceptServer(inbound.data)))
}
}
return true
}
companion object {
private const val CONNECT_TIMEOUT = 5000
}
}