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;
}