From f7f2d781bc6c10a83cc05002e288a67152555f0d Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Tue, 20 Aug 2024 18:09:20 +0200 Subject: [PATCH] work for a bit --- PROTOCOL.md | 48 +++++++++++ pom.xml | 4 + .../eu/m724/auth/master/AccountService.java | 16 ++++ .../eu/m724/websocket/WebsocketResource.java | 76 +++++++++++++++++ .../eu/m724/websocket/WebsocketService.java | 83 +++++++++++++++++++ .../eu/m724/websocket/packet/EmptyPacket.java | 23 +++++ .../java/eu/m724/websocket/packet/Packet.java | 16 ++++ src/main/resources/application.properties | 3 +- 8 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 PROTOCOL.md create mode 100644 src/main/java/eu/m724/websocket/WebsocketResource.java create mode 100644 src/main/java/eu/m724/websocket/WebsocketService.java create mode 100644 src/main/java/eu/m724/websocket/packet/EmptyPacket.java create mode 100644 src/main/java/eu/m724/websocket/packet/Packet.java diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..4c217e2 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,48 @@ +This file documents the websocket /api/ws + +## Format +Packet id then arguments \ +Some packets don't have arguments so send just the packet id \ +There can be multiple packets in one message, those with variable length arguments have to be terminated + +## Authentication +The first message from the server is: +- `0x6d 0x73` + +The client should reply, in a single message: +1. `0xb6 0xc4` +2. client version (byte), right now it's 0 +3. length of access key (byte) +4. access key, decoded from base64 + +Authentication complete, the server will send a disconnect if something's wrong, otherwise it will pong. \ +If the client version is incorrect, the server sends a 0x01 disconnect, and doesn't verify the access key. + +## Commands +### Client -> Server +- `0x00` - Ping + * no body + * the server replies with 0x00 Pong + * also a keepalive, sent by client in at most 30 second intervals otherwise the server disconnects +- `0x01` - Disconnect + * first there's the reason as byte, see below + * second argument is a signed byte - message length in bytes + * then the utf8 encoded message + * after that the server doesn't wait for a reply it just closes the connection +- `0x02` - Settings + * setting id followed by value (length varies) terminated with 0x00 + +### Server -> Client +- `0x00` - Pong + * the body is a single signed long (8 bytes) which the current unix time + * a response to client's command of the same id + +## Disconnect reasons +- `0x00` - unspecified +- `0x01` - incompatible client +- `0x02` - timeout (client didn't send ping) +- `0x03` - access key revoked +- `0x04` - server error + +## Settings +- `0x00` - not used \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0848469..0afec70 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,10 @@ io.quarkus quarkus-jdbc-h2 + + io.quarkus + quarkus-websockets + diff --git a/src/main/java/eu/m724/auth/master/AccountService.java b/src/main/java/eu/m724/auth/master/AccountService.java index 6346fe3..639a023 100644 --- a/src/main/java/eu/m724/auth/master/AccountService.java +++ b/src/main/java/eu/m724/auth/master/AccountService.java @@ -29,6 +29,22 @@ public class AccountService { } } + /** + * gets an access key + * @param bytes access key as bytes + * @return the {@link AccessKey} if correct else null even if key is null + */ + @Transactional + public AccessKey findByAccessKey(byte[] bytes) { + if (bytes == null) return null; + + try { + return AccessKey.find("key", (Object) bytes).firstResult(); + } catch (IllegalArgumentException e) { + return null; + } + } + // TODO maybe move some of these methods somewhere else and reconsider making them static /** diff --git a/src/main/java/eu/m724/websocket/WebsocketResource.java b/src/main/java/eu/m724/websocket/WebsocketResource.java new file mode 100644 index 0000000..201a26c --- /dev/null +++ b/src/main/java/eu/m724/websocket/WebsocketResource.java @@ -0,0 +1,76 @@ +package eu.m724.websocket; + +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; + +import java.nio.ByteBuffer; + +@ApplicationScoped +@ServerEndpoint("/api/ws") +public class WebsocketResource { + @Inject + WebsocketService websocketService; + + @OnOpen + public void onOpen(Session session) { + websocketService.addSession(session.getId()); + System.out.printf("WS [%s]: connected\n", session.getId()); + + session.getAsyncRemote().sendBinary(ByteBuffer.wrap(new byte[] { 0x6d, 0x73 })); + } + + @OnClose + public void onClose(Session session) { + websocketService.removeConnection(session.getId()); + System.out.printf("WS [%s]: disconnected, authenticated: %b\n", session.getId(), websocketService.isAuthenticated(session.getId())); + } + + @OnMessage + public void onMessage(byte[] message, Session session) { + String sessionId = session.getId(); + ByteBuffer command = ByteBuffer.wrap(message); + + if (!websocketService.isAuthenticated(session.getId())) { + if (command.get() == (byte)0xb6 && command.get() == (byte)0xc4) { + byte clientVersion = command.get(); + if (clientVersion == websocketService.protocolVersion) { + byte keyLength = command.get(); + byte[] accessKey = new byte[keyLength]; + command.get(accessKey); + boolean success = websocketService.authenticate(sessionId, accessKey); + } else { + session + } + } + return; + } + + switch (message[0]) { + case 0x00: + pong(session); + break; + + } + } + + private void pong(Session session) { + ByteBuffer byteBuffer = ByteBuffer.allocate(9); + byteBuffer.put((byte)0); + byteBuffer.putLong(System.currentTimeMillis()); + session.getAsyncRemote().sendBinary(byteBuffer); + } + + + /*private void broadcast(String message) { + sessions.values().forEach(s -> { + s.getAsyncRemote().sendObject(message, result -> { + if (result.getException() != null) { + System.out.println("Unable to send message: " + result.getException()); + } + }); + });*/ + } +} diff --git a/src/main/java/eu/m724/websocket/WebsocketService.java b/src/main/java/eu/m724/websocket/WebsocketService.java new file mode 100644 index 0000000..1aa54a0 --- /dev/null +++ b/src/main/java/eu/m724/websocket/WebsocketService.java @@ -0,0 +1,83 @@ +package eu.m724.websocket; + +import eu.m724.auth.master.AccountService; +import eu.m724.orm.AccessKey; +import eu.m724.orm.Account; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.websocket.Session; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@ApplicationScoped +public class WebsocketService { + @ConfigProperty(name = "rwws.protocol_version") + byte protocolVersion; + + @Inject + AccountService accountService; + + private final Map accounts = new ConcurrentHashMap<>(); + + void addSession(String sessionId) { + accounts.put(sessionId, null); + } + + void removeConnection(String sessionId) { + accounts.remove(sessionId); + } + + void disconnect(Session session, DisconnectReason reason, String message) { + byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); + ByteBuffer byteBuffer = ByteBuffer.allocate(3 + messageBytes.length); + byteBuffer.put(0x0) + + session.getAsyncRemote().sendBinary( + ByteBuffer.wrap(new byte[] { reason }); + ) + } + + boolean authenticate(String sessionId, byte[] bytes) { + AccessKey accessKey = accountService.findByAccessKey(byte); + + if (ac == null) + return false; + + accounts.put(sessionId, account); + return true; + } + + boolean isAuthenticated(String sessionId) { + return accounts.containsKey(sessionId); + } + + public enum Packet { + PING((byte)0x00), + DISCONNECT((byte)0x01), + SETTINGS((byte)0x02); + + public final byte value; + + Packet(byte value) { + this.value = value; + } + } + + public enum DisconnectReason { + UNSPECIFIED((byte)0x00), + VERSION_MISMATCH((byte)0x01), + TIMEOUT((byte)0x02), + ACCESS_KEY_REVOKED((byte)0x03), + SERVER_ERROR((byte)0x04); + + public final byte value; + + DisconnectReason(byte value) { + this.value = value; + } + } +} diff --git a/src/main/java/eu/m724/websocket/packet/EmptyPacket.java b/src/main/java/eu/m724/websocket/packet/EmptyPacket.java new file mode 100644 index 0000000..38e508f --- /dev/null +++ b/src/main/java/eu/m724/websocket/packet/EmptyPacket.java @@ -0,0 +1,23 @@ +package eu.m724.websocket.packet; + +import java.nio.ByteBuffer; + +public class EmptyPacket implements Packet { + private final byte packetId; + public final ByteBuffer byteBuffer; + + public EmptyPacket(byte packetId) { + this.packetId = packetId; + this.byteBuffer = ByteBuffer.wrap(new byte[] { packetId }); + } + + @Override + public byte packetId() { + return packetId; + } + + @Override + public ByteBuffer compose() { + return byteBuffer; + } +} diff --git a/src/main/java/eu/m724/websocket/packet/Packet.java b/src/main/java/eu/m724/websocket/packet/Packet.java new file mode 100644 index 0000000..96451be --- /dev/null +++ b/src/main/java/eu/m724/websocket/packet/Packet.java @@ -0,0 +1,16 @@ +package eu.m724.websocket.packet; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +public interface Packet { + byte packetId(); + + default List arguments() { + return Collections.emptyList(); + } + + ByteBuffer compose(); + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 24fc8b2..7240f6b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,4 @@ quarkus.datasource.db.kind=h2 +quarkus.hibernate-orm.database.generation=drop-and-create -quarkus.hibernate-orm.database.generation=drop-and-create \ No newline at end of file +rwws.protocol_version=0 \ No newline at end of file