diff --git a/build.gradle.kts b/build.gradle.kts index be62da480..3749c1d88 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,12 +33,14 @@ sourceSets { dependencies { implementation(project(":pswgcommon")) implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) implementation(group="org.xerial", name="sqlite-jdbc", version="3.30.1") implementation(group="org.mongodb", name="mongodb-driver-sync", version="3.12.2") implementation(group="me.joshlarson", name="fast-json", version="3.0.1") implementation(group="me.joshlarson", name="jlcommon-network", version="1.1.0") implementation(group="me.joshlarson", name="jlcommon-argparse", version="0.9.5") + implementation(group="me.joshlarson", name="websocket", version="0.9.3") implementation(group="com.github.madsboddum", name="swgterrain", version="1.1.3") } } diff --git a/pswgcommon b/pswgcommon index 3fb3cf647..0b12cc371 160000 --- a/pswgcommon +++ b/pswgcommon @@ -1 +1 @@ -Subproject commit 3fb3cf6473a11adc0fd2d1b7aca15a1cec22f4a8 +Subproject commit 0b12cc37137cd4770e36d71e844fdc949a122746 diff --git a/src/main/java/com/projectswg/holocore/intents/support/global/login/RequestLoginIntent.kt b/src/main/java/com/projectswg/holocore/intents/support/global/login/RequestLoginIntent.kt new file mode 100644 index 000000000..090d6d63e --- /dev/null +++ b/src/main/java/com/projectswg/holocore/intents/support/global/login/RequestLoginIntent.kt @@ -0,0 +1,34 @@ +/*********************************************************************************** + * Copyright (c) 2022 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ + +package com.projectswg.holocore.intents.support.global.login + +import com.projectswg.holocore.resources.support.global.player.Player +import me.joshlarson.jlcommon.control.Intent +import java.net.SocketAddress + +class RequestLoginIntent(val player: Player, val username: String, val password: String, val clientVersion: String, val socketAddress: SocketAddress) : Intent() diff --git a/src/main/java/com/projectswg/holocore/resources/support/global/network/NetworkClient.kt b/src/main/java/com/projectswg/holocore/resources/support/global/network/NetworkClient.kt index 6dd01e20b..6995befce 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/global/network/NetworkClient.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/global/network/NetworkClient.kt @@ -28,6 +28,7 @@ package com.projectswg.holocore.resources.support.global.network import com.projectswg.common.network.NetBuffer import com.projectswg.common.network.NetworkProtocol +import com.projectswg.common.network.packets.PacketType import com.projectswg.common.network.packets.SWGPacket import com.projectswg.common.network.packets.swg.ErrorMessage import com.projectswg.common.network.packets.swg.admin.AdminPacket @@ -35,6 +36,8 @@ 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.HoloConnectionStopped.ConnectionStoppedReason import com.projectswg.common.network.packets.swg.holo.HoloSetProtocolVersion +import com.projectswg.common.network.packets.swg.zone.object_controller.ObjectController +import com.projectswg.holocore.intents.support.global.login.RequestLoginIntent import com.projectswg.holocore.intents.support.global.network.ConnectionClosedIntent import com.projectswg.holocore.intents.support.global.network.ConnectionOpenedIntent import com.projectswg.holocore.intents.support.global.network.InboundPacketIntent @@ -42,63 +45,91 @@ import com.projectswg.holocore.resources.support.data.server_info.StandardLog import com.projectswg.holocore.resources.support.global.player.AccessLevel import com.projectswg.holocore.resources.support.global.player.Player import me.joshlarson.jlcommon.control.IntentChain +import me.joshlarson.jlcommon.log.Log +import me.joshlarson.websocket.common.WebSocketHandler +import me.joshlarson.websocket.common.parser.http.HttpRequest +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 me.joshlarson.websocket.server.WebSocketServerCallback +import me.joshlarson.websocket.server.WebSocketServerProtocol import java.net.InetSocketAddress import java.net.SocketAddress import java.nio.ByteBuffer +import java.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference -import java.util.function.Consumer +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock -class NetworkClient(private val remoteAddress: SocketAddress, private val write: (ByteBuffer) -> Unit, private val closeChannel: () -> Unit): TCPServerChannel { +class NetworkClient(private val remoteAddress: SocketAddress, write: (ByteBuffer) -> Unit, closeChannel: () -> Unit): TCPServerChannel, WebSocketServerCallback { - private val inboundBuffer: NetBuffer - private val intentChain: IntentChain - private val connected: AtomicBoolean - private val status: AtomicReference - val player: Player + private val inboundBuffer = ByteBuffer.allocate(INBOUND_BUFFER_SIZE) + private val intentChain = IntentChain() + private val connected = AtomicBoolean(true) + private val status = AtomicReference(SessionStatus.DISCONNECTED) + private val wsProtocol = WebSocketServerProtocol(this, { data -> write(ByteBuffer.wrap(data)) }, closeChannel) + private val writeLock = ReentrantLock() + + val player = Player(SESSION_ID.getAndIncrement(), remoteAddress as InetSocketAddress?) { this.addToOutbound(it) } val id: Long get() = player.networkId - private var clientDisconnectReason: ConnectionStoppedReason - private var serverDisconnectReason: ConnectionStoppedReason - - init { - this.inboundBuffer = NetBuffer.allocate(INBOUND_BUFFER_SIZE) - - this.intentChain = IntentChain() - this.connected = AtomicBoolean(true) - this.status = AtomicReference(SessionStatus.DISCONNECTED) - this.player = Player(SESSION_ID.getAndIncrement(), remoteAddress as InetSocketAddress?, Consumer { this.addToOutbound(it) }) - this.clientDisconnectReason = ConnectionStoppedReason.UNKNOWN - this.serverDisconnectReason = ConnectionStoppedReason.UNKNOWN - } + private var clientDisconnectReason = ConnectionStoppedReason.UNKNOWN + private var serverDisconnectReason = ConnectionStoppedReason.UNKNOWN @JvmOverloads fun close(reason: ConnectionStoppedReason = ConnectionStoppedReason.OTHER_SIDE_TERMINATED) { if (connected.getAndSet(false)) { serverDisconnectReason = reason - write(NetworkProtocol.encode(HoloConnectionStopped(reason)).buffer) - closeChannel() + wsProtocol.sendClose(WebSocketCloseReason.NORMAL.statusCode.toInt(), reason.name) } } override fun getChannelBuffer(): ByteBuffer { - return inboundBuffer.buffer + return inboundBuffer } override fun onRead() { - inboundBuffer.flip() - while (true) { - val p = NetworkProtocol.decode(inboundBuffer) ?: break - if (!allowInbound(p)) - continue - p.socketAddress = remoteAddress - processPacket(p) - intentChain.broadcastAfter(InboundPacketIntent(player, p)) + wsProtocol.onRead(inboundBuffer.array(), 0, inboundBuffer.position()) + inboundBuffer.position(0) + } + + override fun onUpgrade(obj: WebSocketHandler, request: HttpRequest) { + val urlParameters = request.urlParameters + val username = getUrlParameter(urlParameters, "username", true) ?: return + val password = getUrlParameter(urlParameters, "password", true) ?: return + val protocolVersion = getUrlParameter(urlParameters, "protocolVersion", true) ?: return + StandardLog.onPlayerTrace(this, player, "requested login for $username and protocol version $protocolVersion") + + if (protocolVersion == NetworkProtocol.VERSION) { + onConnected() + } else { + close(ConnectionStoppedReason.INVALID_PROTOCOL) + return + } + + RequestLoginIntent(player, username, password, "20051010-17:00", remoteAddress).broadcast() + } + + override fun onBinaryMessage(obj: WebSocketHandler, data: ByteArray) { + if (data.size < 6) + return + val swg = NetBuffer.wrap(data) + + swg.position(2) + val crc: Int = swg.int + swg.position(0) + + if (crc == ObjectController.CRC) { + onInbound(ObjectController.decodeController(swg)) + } else { + val packet = PacketType.getForCrc(crc) + packet?.decode(swg) + onInbound(packet) } - inboundBuffer.compact() } override fun onOpened() { @@ -116,9 +147,43 @@ class NetworkClient(private val remoteAddress: SocketAddress, private val write: return "NetworkClient[$remoteAddress]" } + private fun getUrlParameter(urlParameters: Map>, key: String, required: Boolean): String? { + if (!urlParameters.containsKey(key)) { + if (required) { + StandardLog.onPlayerError(this, player, "onUpgrade: no $key specified - disconnecting") + close(ConnectionStoppedReason.APPLICATION) + } + return null + } + val encodedValues = urlParameters[key] + if (encodedValues?.size != 1) { + if (required) { + StandardLog.onPlayerError(this, player, "onUpgrade: invalid count of $key: ${encodedValues?.size ?: -1}") + close(ConnectionStoppedReason.APPLICATION) + } + return null + } + + return String(Base64.getDecoder().decode(encodedValues[0])) + } + + private fun onInbound(p: SWGPacket?) { + if (p == null || !allowInbound(p)) + return + p.socketAddress = remoteAddress + processPacket(p) + intentChain.broadcastAfter(InboundPacketIntent(player, p)) + } + private fun addToOutbound(p: SWGPacket) { if (allowOutbound(p) && connected.get()) { - write(NetworkProtocol.encode(p).buffer) + val encoded = p.encode() + if (encoded.position() != encoded.capacity()) + Log.w("SWGPacket %s has invalid array length. Expected: %d Actual: %d", p, encoded.remaining(), encoded.capacity()) + + writeLock.withLock { + wsProtocol.send(WebsocketFrame(WebsocketFrameType.BINARY, encoded.buffer.array())) + } } } diff --git a/src/main/java/com/projectswg/holocore/services/support/global/zone/LoginService.java b/src/main/java/com/projectswg/holocore/services/support/global/zone/LoginService.java index bbd5dd17b..63a46547e 100644 --- a/src/main/java/com/projectswg/holocore/services/support/global/zone/LoginService.java +++ b/src/main/java/com/projectswg/holocore/services/support/global/zone/LoginService.java @@ -42,6 +42,7 @@ import com.projectswg.common.network.packets.swg.zone.ServerNowEpochTime; import com.projectswg.holocore.ProjectSWG; import com.projectswg.holocore.intents.support.global.login.LoginEventIntent; import com.projectswg.holocore.intents.support.global.login.LoginEventIntent.LoginEvent; +import com.projectswg.holocore.intents.support.global.login.RequestLoginIntent; import com.projectswg.holocore.intents.support.global.network.CloseConnectionIntent; import com.projectswg.holocore.intents.support.global.network.InboundPacketIntent; import com.projectswg.holocore.intents.support.global.zone.creation.DeleteCharacterIntent; @@ -61,6 +62,7 @@ import com.projectswg.holocore.services.support.objects.ObjectStorageService.Obj import me.joshlarson.jlcommon.control.IntentHandler; import me.joshlarson.jlcommon.control.Service; +import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -82,9 +84,8 @@ public class LoginService extends Service { @IntentHandler private void handleObjectCreatedIntent(ObjectCreatedIntent oci) { SWGObject obj = oci.getObject(); - if (!(obj instanceof PlayerObject)) + if (!(obj instanceof PlayerObject player)) return; - PlayerObject player = (PlayerObject) obj; CreatureObject creature = (CreatureObject) player.getParent(); if (creature == null) return; @@ -107,8 +108,8 @@ public class LoginService extends Service { SWGPacket p = gpi.getPacket(); if (p instanceof HoloLoginRequestPacket) { handleLogin(gpi.getPlayer(), (HoloLoginRequestPacket) p); - } else if (p instanceof LoginClientId) { - handleLogin(gpi.getPlayer(), (LoginClientId) p); + } else if (p instanceof LoginClientId id) { + handleLogin(gpi.getPlayer(), id.getUsername(), id.getPassword(), id.getVersion(), id.getSocketAddress()); } else if (p instanceof DeleteCharacterRequest) { handleCharDeletion(gpi.getPlayer(), (DeleteCharacterRequest) p); } else if (p instanceof LagRequest) { @@ -118,6 +119,11 @@ public class LoginService extends Service { } } + @IntentHandler + private void handleRequestLoginIntent(RequestLoginIntent rli) { + handleLogin(rli.getPlayer(), rli.getUsername(), rli.getPassword(), rli.getClientVersion(), rli.getSocketAddress()); + } + private String getServerString() { String name = PswgDatabase.INSTANCE.getConfig().getString(this, "loginServerName", "LoginServer"); int id = PswgDatabase.INSTANCE.getConfig().getInt(this, "loginServerId", 1); @@ -183,7 +189,7 @@ public class LoginService extends Service { } } - private void handleLogin(Player player, LoginClientId id) { + private void handleLogin(Player player, String username, String password, String clientVersion, SocketAddress socketAddress) { if (player.getPlayerState() == PlayerState.LOGGED_IN) { // Client occasionally sends multiple login requests sendLoginSuccessPacket(player); return; @@ -193,37 +199,37 @@ public class LoginService extends Service { player.setPlayerState(PlayerState.LOGGING_IN); player.setPlayerServer(PlayerServer.LOGIN); final boolean doClientCheck = PswgDatabase.INSTANCE.getConfig().getBoolean(this, "loginVersionChecks", true); - if (!id.getVersion().equals(REQUIRED_VERSION) && doClientCheck) { - StandardLog.onPlayerEvent(this, player, "failed to login [incorrect version: %s] from %s", id.getVersion(), id.getSocketAddress()); - onLoginClientVersionError(player, id); + if (!clientVersion.equals(REQUIRED_VERSION) && doClientCheck) { + StandardLog.onPlayerEvent(this, player, "failed to login [incorrect version: %s] from %s", clientVersion, socketAddress); + onLoginClientVersionError(player, clientVersion); return; } - UserMetadata user = PswgDatabase.INSTANCE.getUsers().getUser(id.getUsername()); - player.setUsername(id.getUsername()); + UserMetadata user = PswgDatabase.INSTANCE.getUsers().getUser(username); + player.setUsername(username); if (user == null) { - StandardLog.onPlayerEvent(this, player, "failed to login [incorrect username] from %s", id.getSocketAddress()); + StandardLog.onPlayerEvent(this, player, "failed to login [incorrect username] from %s", socketAddress); onInvalidUserPass(player); player.sendPacket(new ErrorMessage("Login Failed!", "Incorrect username", false)); player.sendPacket(new LoginIncorrectClientId(getServerString(), REQUIRED_VERSION)); } else if (user.isBanned()) { - StandardLog.onPlayerEvent(this, player, "failed to login [banned] from %s", id.getSocketAddress()); + StandardLog.onPlayerEvent(this, player, "failed to login [banned] from %s", socketAddress); onLoginBanned(player); player.sendPacket(new ErrorMessage("Login Failed!", "Sorry, you're banned!", false)); - } else if (isUserValid(user, id.getPassword())) { - StandardLog.onPlayerEvent(this, player, "logged in from %s", id.getSocketAddress()); + } else if (isUserValid(user, password)) { + StandardLog.onPlayerEvent(this, player, "logged in from %s", socketAddress); onSuccessfulLogin(user, player); sendLoginSuccessPacket(player); } else { - StandardLog.onPlayerEvent(this, player, "failed to login [incorrect password] from %s", id.getSocketAddress()); + StandardLog.onPlayerEvent(this, player, "failed to login [incorrect password] from %s", socketAddress); onInvalidUserPass(player); player.sendPacket(new ErrorMessage("Login Failed!", "Incorrect password", false)); player.sendPacket(new LoginIncorrectClientId(getServerString(), REQUIRED_VERSION)); } } - private void onLoginClientVersionError(Player player, LoginClientId id) { - player.sendPacket(new ErrorMessage("Login Failed!", "Invalid Client Version Code: " + id.getVersion(), false)); + private void onLoginClientVersionError(Player player, String clientVersion) { + player.sendPacket(new ErrorMessage("Login Failed!", "Invalid Client Version Code: " + clientVersion, false)); player.setPlayerState(PlayerState.DISCONNECTED); new LoginEventIntent(player.getNetworkId(), LoginEvent.LOGIN_FAIL_INVALID_VERSION_CODE).broadcast(); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f51d8d08d..71ef26b20 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,10 +10,12 @@ open module holocore { requires me.joshlarson.jlcommon; requires me.joshlarson.jlcommon.network; requires me.joshlarson.jlcommon.argparse; + requires me.joshlarson.websocket; requires com.projectswg.common; requires fast.json; requires kotlin.stdlib; + requires kotlin.reflect; requires swgterrain; }