Changed network protocol to websockets

This commit is contained in:
Josh Larson
2022-07-05 20:36:04 -05:00
parent 586a70ad54
commit 2e9b5baa67
6 changed files with 160 additions and 51 deletions
+2
View File
@@ -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")
}
}
@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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()
@@ -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<SessionStatus>
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<SWGPacket> { 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<String, List<String>>, 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()))
}
}
}
@@ -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();
}
+2
View File
@@ -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;
}