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