diff --git a/.gitmodules b/.gitmodules index 2bf2c93..33ce1d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index fd8b1e4..5b3cabf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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().configureEach kotlinOptions { jvmTarget = kotlinTargetJdk } - destinationDir = sourceSets.main.get().java.outputDir + destinationDirectory.set(File(destinationDirectory.get().asFile.path.replace("kotlin", "java"))) } diff --git a/client-holocore b/client-holocore deleted file mode 160000 index 42894ee..0000000 --- a/client-holocore +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 42894ee7e1bab447eb9d669b6314e3feffcaf02d diff --git a/settings.gradle.kts b/settings.gradle.kts index 05ac5c0..ce0d209 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -include(":pswgcommon", ":client-holocore") +include(":pswgcommon") diff --git a/src/main/java/com/projectswg/forwarder/Forwarder.kt b/src/main/java/com/projectswg/forwarder/Forwarder.kt index 4a23839..ba69aa7 100644 --- a/src/main/java/com/projectswg/forwarder/Forwarder.kt +++ b/src/main/java/com/projectswg/forwarder/Forwarder.kt @@ -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) { - SafeMain.main("") { mainRunnable() } - } - - private fun mainRunnable() { - Log.addWrapper(ConsoleLogWrapper(LogLevel.TRACE)) - val forwarder = Forwarder() - forwarder.data.address = InetSocketAddress(44463) - forwarder.run() - ThreadUtilities.printActiveThreads() - } } } diff --git a/src/main/java/com/projectswg/forwarder/resources/networking/NetInterceptor.kt b/src/main/java/com/projectswg/forwarder/resources/networking/NetInterceptor.kt index ebb5afe..169cc53 100644 --- a/src/main/java/com/projectswg/forwarder/resources/networking/NetInterceptor.kt +++ b/src/main/java/com/projectswg/forwarder/resources/networking/NetInterceptor.kt @@ -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 diff --git a/src/main/java/com/projectswg/forwarder/resources/server/HolocoreConnection.kt b/src/main/java/com/projectswg/forwarder/resources/server/HolocoreConnection.kt new file mode 100644 index 0000000..a2c568f --- /dev/null +++ b/src/main/java/com/projectswg/forwarder/resources/server/HolocoreConnection.kt @@ -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(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() + } + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/projectswg/forwarder/services/client/ClientOutboundDataService.kt b/src/main/java/com/projectswg/forwarder/services/client/ClientOutboundDataService.kt index a88b2e5..f4d2254 100644 --- a/src/main/java/com/projectswg/forwarder/services/client/ClientOutboundDataService.kt +++ b/src/main/java/com/projectswg/forwarder/services/client/ClientOutboundDataService.kt @@ -32,8 +32,8 @@ class ClientOutboundDataService : Service() { private val outboundBuffer: Array = arrayOfNulls(4096) private val multiplexer: IntentMultiplexer = IntentMultiplexer(this, ProtocolStack::class.java, Packet::class.java) private val activeStacks: MutableSet = 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() diff --git a/src/main/java/com/projectswg/forwarder/services/client/ClientServerService.kt b/src/main/java/com/projectswg/forwarder/services/client/ClientServerService.kt index c07884d..420d6ec 100644 --- a/src/main/java/com/projectswg/forwarder/services/client/ClientServerService.kt +++ b/src/main/java/com/projectswg/forwarder/services/client/ClientServerService.kt @@ -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 { 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 { 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 { 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) } diff --git a/src/main/java/com/projectswg/forwarder/services/crash/IntentRecordingService.kt b/src/main/java/com/projectswg/forwarder/services/crash/IntentRecordingService.kt index 1c5201e..2501733 100644 --- a/src/main/java/com/projectswg/forwarder/services/crash/IntentRecordingService.kt +++ b/src/main/java/com/projectswg/forwarder/services/crash/IntentRecordingService.kt @@ -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 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ce6e4bb..df68854 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -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; diff --git a/src/main/kotlin/com/projectswg/forwarder/services/server/ServerConnectionService.kt b/src/main/kotlin/com/projectswg/forwarder/services/server/ServerConnectionService.kt index 3ca4cc2..519c37b 100644 --- a/src/main/kotlin/com/projectswg/forwarder/services/server/ServerConnectionService.kt +++ b/src/main/kotlin/com/projectswg/forwarder/services/server/ServerConnectionService.kt @@ -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(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 - - } - }