diff options
| author | ilotterytea <iltsu@alright.party> | 2025-01-05 00:14:45 +0500 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2025-01-05 00:58:57 +0500 |
| commit | b0f71c097631f6f9d595865c48f1ffd1990e8794 (patch) | |
| tree | 0c2a12ba73a9610c6d7aaaf912b08c39f221a4c7 | |
| parent | 47303cced8f6e55aaab2579293da49723ba39c1d (diff) | |
13 files changed, 333 insertions, 5 deletions
diff --git a/core/src/kz/ilotterytea/maxon/MaxonConstants.java b/core/src/kz/ilotterytea/maxon/MaxonConstants.java index cab66f4..44c7208 100644 --- a/core/src/kz/ilotterytea/maxon/MaxonConstants.java +++ b/core/src/kz/ilotterytea/maxon/MaxonConstants.java @@ -62,6 +62,7 @@ public class MaxonConstants { public static final String IDENTITY_REFRESH_URL = IDENTITY_BASE_URL + "/refresh"; public static final String IDENTITY_AUTHENTICATION_URL = IDENTITY_BASE_URL + "/authenticate"; public static final String IDENTITY_REGISTRATION_URL = IDENTITY_BASE_URL; + public static final String SESSION_WSS_URL = "ws://localhost:31084"; public static final long DISCORD_APPLICATION_ID = 1051092609659052062L; diff --git a/core/src/kz/ilotterytea/maxon/MaxonGame.java b/core/src/kz/ilotterytea/maxon/MaxonGame.java index 8986de9..7381b6d 100644 --- a/core/src/kz/ilotterytea/maxon/MaxonGame.java +++ b/core/src/kz/ilotterytea/maxon/MaxonGame.java @@ -9,6 +9,7 @@ import kz.ilotterytea.maxon.localization.LocalizationManager; import kz.ilotterytea.maxon.pets.PetManager; import kz.ilotterytea.maxon.screens.SplashScreen; import kz.ilotterytea.maxon.session.IdentityClient; +import kz.ilotterytea.maxon.session.SessionClient; import kz.ilotterytea.maxon.utils.GameUpdater; public class MaxonGame extends Game { @@ -20,7 +21,9 @@ public class MaxonGame extends Game { private PetManager petManager; private DiscordActivityClient discordActivityClient; + private IdentityClient identityClient; + private SessionClient sessionClient; private static MaxonGame instance; @@ -43,6 +46,10 @@ public class MaxonGame extends Game { return identityClient; } + public SessionClient getSessionClient() { + return sessionClient; + } + public LocalizationManager getLocale() { return locale; } @@ -57,6 +64,8 @@ public class MaxonGame extends Game { new GameUpdater().checkLatestUpdate(); identityClient = new IdentityClient(Gdx.app.getPreferences("kz.ilotterytea.SigninIdentity")); + sessionClient = new SessionClient(); + batch = new SpriteBatch(); prefs = Gdx.app.getPreferences(MaxonConstants.GAME_APP_PACKAGE); locale = new LocalizationManager(Gdx.files.internal("i18n/" + prefs.getString("lang", "en_us") + ".json")); diff --git a/core/src/kz/ilotterytea/maxon/session/IdentityClient.java b/core/src/kz/ilotterytea/maxon/session/IdentityClient.java index 570b16b..831589d 100644 --- a/core/src/kz/ilotterytea/maxon/session/IdentityClient.java +++ b/core/src/kz/ilotterytea/maxon/session/IdentityClient.java @@ -10,6 +10,7 @@ import com.badlogic.gdx.utils.JsonValue; import com.badlogic.gdx.utils.JsonWriter; import com.badlogic.gdx.utils.Timer; import kz.ilotterytea.maxon.MaxonConstants; +import kz.ilotterytea.maxon.MaxonGame; import kz.ilotterytea.maxon.utils.RandomUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,6 +91,8 @@ public class IdentityClient { IdentityClient.this.userId = String.valueOf(json.get("data").get("user").getInt("id")); IdentityClient.this.isAuthorised = true; log.info("Successfully authorised! Welcome back, {}!", IdentityClient.this.username); + + MaxonGame.getInstance().getSessionClient().connect(); } catch (Exception e) { log.error("An exception was thrown while authorising", e); } @@ -141,6 +144,7 @@ public class IdentityClient { accessToken = null; userId = null; isAuthorised = false; + MaxonGame.getInstance().getSessionClient().close(5001, "Failed to validate token"); authorize(username, password); return; } @@ -213,6 +217,8 @@ public class IdentityClient { sessionPreferences.remove("username"); sessionPreferences.remove("password"); sessionPreferences.flush(); + + MaxonGame.getInstance().getSessionClient().close(5001, "Invalidated token"); } catch (Exception ignored) { } } @@ -262,11 +268,14 @@ public class IdentityClient { userId = null; isAuthorised = false; log.warn(error); + MaxonGame.getInstance().getSessionClient().close(5002, "Failed to refresh token"); return; } accessToken = json.get("data").get("accessToken").asString(); log.info("Token has been refreshed!"); + + MaxonGame.getInstance().getSessionClient().updateIdentity(); } catch (Exception e) { log.error("An exception was thrown while refreshing", e); } diff --git a/core/src/kz/ilotterytea/maxon/session/SessionClient.java b/core/src/kz/ilotterytea/maxon/session/SessionClient.java new file mode 100644 index 0000000..78a6c0e --- /dev/null +++ b/core/src/kz/ilotterytea/maxon/session/SessionClient.java @@ -0,0 +1,95 @@ +package kz.ilotterytea.maxon.session; + +import kz.ilotterytea.maxon.MaxonConstants; +import kz.ilotterytea.maxon.MaxonGame; +import kz.ilotterytea.maxon.screens.MenuScreen; +import kz.ilotterytea.maxon.shared.Acknowledge; +import kz.ilotterytea.maxon.shared.Identity; +import kz.ilotterytea.maxon.shared.exceptions.PlayerKickException; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URI; +import java.nio.ByteBuffer; + +public class SessionClient extends WebSocketClient { + private final Logger log; + private final MaxonGame game; + + public SessionClient() { + super(URI.create(MaxonConstants.SESSION_WSS_URL)); + this.log = LoggerFactory.getLogger(SessionClient.class); + this.game = MaxonGame.getInstance(); + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + log.info("Connected!"); + updateIdentity(); + } + + @Override + public void onMessage(String message) { + } + + @Override + public void onMessage(ByteBuffer bytes) { + try { + // Deserialize the object + ByteArrayInputStream bais = new ByteArrayInputStream(bytes.array()); + ObjectInputStream ois = new ObjectInputStream(bais); + Object obj = ois.readObject(); + + if (obj instanceof Acknowledge acknowledge) SessionHandlers.handleAcknowledge(acknowledge); + else if (obj instanceof PlayerKickException exception) throw exception; + } catch (PlayerKickException e) { + log.info("Kicked out!", e); + } catch (Exception e) { + log.error("An exception was thrown while processing message", e); + } + } + + @Override + public void onClose(int code, String reason, boolean remote) { + log.info("Connection closed! Reason: {} {}", code, reason); + game.getIdentityClient().invalidateToken(); + if (!game.getScreen().getClass().equals(MenuScreen.class)) { + game.setScreen(new MenuScreen()); + } + } + + @Override + public void onError(Exception ex) { + log.error("Failed to connect", ex); + } + + @Override + public void connect() { + super.connect(); + } + + public void updateIdentity() { + IdentityClient identityClient = game.getIdentityClient(); + + Identity identity = new Identity(identityClient.getClientToken(), identityClient.getAccessToken()); + + send(identity); + } + + public void send(Object object) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(object); + send(baos.toByteArray()); + } catch (Exception e) { + log.error("Failed to serialize and send an object", e); + } + } +} diff --git a/core/src/kz/ilotterytea/maxon/session/SessionHandlers.java b/core/src/kz/ilotterytea/maxon/session/SessionHandlers.java new file mode 100644 index 0000000..7b86916 --- /dev/null +++ b/core/src/kz/ilotterytea/maxon/session/SessionHandlers.java @@ -0,0 +1,15 @@ +package kz.ilotterytea.maxon.session; + +import kz.ilotterytea.maxon.MaxonGame; +import kz.ilotterytea.maxon.shared.Acknowledge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionHandlers { + private static final Logger log = LoggerFactory.getLogger(SessionHandlers.class); + private static final SessionClient client = MaxonGame.getInstance().getSessionClient(); + + public static void handleAcknowledge(Acknowledge acknowledge) { + log.info("alright: {}", acknowledge); + } +} diff --git a/core/src/main/resources/logback.xml b/core/src/main/resources/logback.xml index 4b08185..df24417 100644 --- a/core/src/main/resources/logback.xml +++ b/core/src/main/resources/logback.xml @@ -9,7 +9,7 @@ </encoder> </appender> - <root level="info"> + <root level="debug"> <appender-ref ref="STDOUT"/> </root> </configuration> diff --git a/server/src/kz/ilotterytea/maxon/MaxonServer.java b/server/src/kz/ilotterytea/maxon/MaxonServer.java index 7a38a7f..91ba049 100644 --- a/server/src/kz/ilotterytea/maxon/MaxonServer.java +++ b/server/src/kz/ilotterytea/maxon/MaxonServer.java @@ -1,36 +1,83 @@ package kz.ilotterytea.maxon; +import kz.ilotterytea.maxon.shared.Identity; +import kz.ilotterytea.maxon.shared.exceptions.PlayerKickException; import org.java_websocket.WebSocket; +import org.java_websocket.framing.CloseFrame; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Optional; public class MaxonServer extends WebSocketServer { private static MaxonServer instance; private final Logger log; + private final ArrayList<PlayerConnection> connections; private MaxonServer() { super(new InetSocketAddress(31084)); this.log = LoggerFactory.getLogger(MaxonServer.class); + this.connections = new ArrayList<>(); } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { - log.info("{} connected!", conn.getRemoteSocketAddress().getAddress().getHostAddress()); + PlayerConnection connection = new PlayerConnection(conn); + log.info("{} ({}) connected!", connection, conn.getRemoteSocketAddress().getAddress().getHostAddress()); + this.connections.add(connection); } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { - log.info("Connection {} has been closed! ({} {} {})", conn, code, reason, remote); + Optional<PlayerConnection> connection = this.connections.stream().filter((x) -> x.getConnection().equals(conn)).findFirst(); + if (connection.isPresent()) { + log.info("{} has left! Reason: {} {}", connection.get(), code, reason); + this.connections.remove(connection.get()); + } else { + log.info("Unknown connection was closed! Reason: {} {}", code, reason); + } } @Override public void onMessage(WebSocket conn, String message) { - log.info("{} says {}", conn, message); + this.connections.removeIf((x) -> x.getConnection().equals(conn)); + conn.send("Invalid input."); + conn.close(CloseFrame.UNEXPECTED_CONDITION); + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer message) { + Optional<PlayerConnection> playerConnection = this.connections.stream().filter((x) -> x.getConnection().equals(conn)).findFirst(); + + if (playerConnection.isEmpty()) { + conn.close(5001, "Your PlayerConnection was not found!"); + return; + } + + PlayerConnection c = playerConnection.get(); + + try { + // Deserialize the object + ByteArrayInputStream bais = new ByteArrayInputStream(message.array()); + ObjectInputStream ois = new ObjectInputStream(bais); + Object obj = ois.readObject(); + + if (obj instanceof Identity identity) ServerHandlers.handleIdentity(c, identity); + else kickConnection(c, PlayerKickException.internalServerError()); + } catch (PlayerKickException e) { + kickConnection(c, e); + } catch (Exception e) { + log.error("An exception was thrown while processing message", e); + kickConnection(c, PlayerKickException.internalServerError()); + } } @Override @@ -45,8 +92,19 @@ public class MaxonServer extends WebSocketServer { setConnectionLostTimeout(100); } + public void kickConnection(PlayerConnection connection, PlayerKickException e) { + connection.send(e); + connection.getConnection().close(); + this.connections.remove(connection); + log.debug("Kicked out {}! Reason: {}", connection, e.getMessage()); + } + public static MaxonServer getInstance() { if (instance == null) instance = new MaxonServer(); return instance; } + + public ArrayList<PlayerConnection> getPlayerConnections() { + return connections; + } } diff --git a/server/src/kz/ilotterytea/maxon/PlayerConnection.java b/server/src/kz/ilotterytea/maxon/PlayerConnection.java new file mode 100644 index 0000000..79b46d0 --- /dev/null +++ b/server/src/kz/ilotterytea/maxon/PlayerConnection.java @@ -0,0 +1,66 @@ +package kz.ilotterytea.maxon; + +import kz.ilotterytea.maxon.shared.Identity; +import org.java_websocket.WebSocket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.sql.Timestamp; + +public class PlayerConnection { + private static int TOTAL_CONNECTION_IDS = 0; + + private final int id; + private final WebSocket connection; + private Identity identity; + + private final Timestamp connectedTimestamp; + + public PlayerConnection(WebSocket connection) { + this.connection = connection; + this.connectedTimestamp = new Timestamp(System.currentTimeMillis()); + + this.id = TOTAL_CONNECTION_IDS; + TOTAL_CONNECTION_IDS++; + } + + public int getId() { + return id; + } + + public WebSocket getConnection() { + return connection; + } + + public Identity getIdentity() { + return identity; + } + + public void setIdentity(Identity identity) { + this.identity = identity; + } + + public Timestamp getConnectedTimestamp() { + return connectedTimestamp; + } + + public void send(Object object) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { + objectOutputStream.writeObject(object); + } catch (IOException ignored) { + + } + + this.connection.send(byteArrayOutputStream.toByteArray()); + } + + @Override + public String toString() { + return "PlayerConnection{" + + "id=" + id + + '}'; + } +} diff --git a/server/src/kz/ilotterytea/maxon/ServerHandlers.java b/server/src/kz/ilotterytea/maxon/ServerHandlers.java new file mode 100644 index 0000000..43468b3 --- /dev/null +++ b/server/src/kz/ilotterytea/maxon/ServerHandlers.java @@ -0,0 +1,27 @@ +package kz.ilotterytea.maxon; + +import kz.ilotterytea.maxon.shared.Acknowledge; +import kz.ilotterytea.maxon.shared.Identity; +import kz.ilotterytea.maxon.shared.exceptions.PlayerKickException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServerHandlers { + private static final Logger log = LoggerFactory.getLogger(ServerHandlers.class); + private static final MaxonServer server = MaxonServer.getInstance(); + + public static void handleIdentity(PlayerConnection connection, Identity identity) { + if (server.getPlayerConnections() + .stream() + .filter((x) -> x.getIdentity() != null) + .anyMatch((x) -> x.getIdentity().equals(identity) && x.getId() != connection.getId()) + ) { + server.kickConnection(connection, PlayerKickException.loggedIn()); + return; + } + + connection.setIdentity(identity); + connection.send(new Acknowledge(identity)); + log.debug("Successfully identified {} for {}", identity, connection); + } +} diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index 4b08185..df24417 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -9,7 +9,7 @@ </encoder> </appender> - <root level="info"> + <root level="debug"> <appender-ref ref="STDOUT"/> </root> </configuration> diff --git a/shared/src/kz/ilotterytea/maxon/shared/Acknowledge.java b/shared/src/kz/ilotterytea/maxon/shared/Acknowledge.java new file mode 100644 index 0000000..b64d3e2 --- /dev/null +++ b/shared/src/kz/ilotterytea/maxon/shared/Acknowledge.java @@ -0,0 +1,12 @@ +package kz.ilotterytea.maxon.shared; + +import java.io.Serializable; + +public record Acknowledge(Object payload) implements Serializable { + @Override + public String toString() { + return "Acknowledge{" + + "payload='" + payload + '\'' + + '}'; + } +} diff --git a/shared/src/kz/ilotterytea/maxon/shared/Identity.java b/shared/src/kz/ilotterytea/maxon/shared/Identity.java new file mode 100644 index 0000000..103c53c --- /dev/null +++ b/shared/src/kz/ilotterytea/maxon/shared/Identity.java @@ -0,0 +1,21 @@ +package kz.ilotterytea.maxon.shared; + +import java.io.Serializable; + +public record Identity(String accessToken, String clientToken) implements Serializable { + @Override + public boolean equals(Object obj) { + if (obj instanceof Identity i) { + return i.accessToken.equals(accessToken) && i.clientToken.equals(clientToken); + } + return false; + } + + @Override + public String toString() { + return "Identity{" + + "clientToken='" + clientToken + '\'' + + ", accessToken='" + accessToken + '\'' + + '}'; + } +} diff --git a/shared/src/kz/ilotterytea/maxon/shared/exceptions/PlayerKickException.java b/shared/src/kz/ilotterytea/maxon/shared/exceptions/PlayerKickException.java new file mode 100644 index 0000000..45d55b3 --- /dev/null +++ b/shared/src/kz/ilotterytea/maxon/shared/exceptions/PlayerKickException.java @@ -0,0 +1,15 @@ +package kz.ilotterytea.maxon.shared.exceptions; + +public class PlayerKickException extends RuntimeException { + public PlayerKickException(String message) { + super(message); + } + + public static PlayerKickException loggedIn() { + return new PlayerKickException("You logged in from another location"); + } + + public static PlayerKickException internalServerError() { + return new PlayerKickException("Internal Server Error"); + } +} |
