diff --git a/PROTOCOL.md b/PROTOCOL.md
index 4c217e2..f804aeb 100644
--- a/PROTOCOL.md
+++ b/PROTOCOL.md
@@ -3,7 +3,8 @@ 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
+There can be multiple packets in one message, those with variable length arguments have to be terminated \
+Numbers are signed
## Authentication
The first message from the server is:
@@ -16,33 +17,35 @@ The client should reply, in a single message:
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.
+No commands are handled during this time
-## 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
+# Commands
+### Server bound (Client -> Server)
+| Code | Name | Data | Notes |
+|--------|----------|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
+| `0x00` | Ping | | the server replies with pong
must be sent by client every at most 30 seconds, otherwise the server disconnects |
+| `0x01` | Settings | 1. byte: setting id
2. value
3. and so on (terminated by 0xFF) | see below for IDs, value type varies |
-### 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
+### Client bound (Server -> Client)
+
+| Code | Name | Data | Notes |
+|--------|------|----------------------|----------------------------------------------------------------------|
+| `0x00` | Pong | 1. long: unix millis | A response to ping, also sent by server on successful authentication |
## Disconnect reasons
-- `0x00` - unspecified
-- `0x01` - incompatible client
-- `0x02` - timeout (client didn't send ping)
-- `0x03` - access key revoked
-- `0x04` - server error
+On every disconnect there's a human-readable message the client should display
+
+| Code | Name | Notes |
+|--------|--------------------|-------------------------------------------------|
+| `3000` | reserved | |
+| `3001` | unauthorized | used during authentication phase |
+| `3002` | version mismatch | incompatible client |
+| `3003` | timeout | client wasn't sending pings |
+| `3004` | access key revoked | when the access key was revoked while connected |
+| `3005` | server error | |
## Settings
-- `0x00` - not used
\ No newline at end of file
+| Code | Name | Notes |
+|--------|------------|-------------------------------------------------|
+| `0x00` | reserved | |
+| `0xFF` | terminator | not a setting, just used to terminate sequences |
\ No newline at end of file
diff --git a/README.md b/README.md
index 28a0a7d..0517460 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,13 @@
# rwws - RealWeather Web Server
-This contacts APIs instead of the servers because we scrape some and we don't want to draw attention
+This contacts APIs instead of the servers because we scrape some, and we don't want to draw attention
### Usage
See `RUNNING.md`[^1]
+### Websocket message format
+See `PROTOCOL.md`
+
---
[^1]: This is the default Quarkus readme that I wish was available online
\ No newline at end of file
diff --git a/src/main/java/eu/m724/websocket/WebsocketResource.java b/src/main/java/eu/m724/websocket/WebsocketResource.java
index 201a26c..c7fd9e8 100644
--- a/src/main/java/eu/m724/websocket/WebsocketResource.java
+++ b/src/main/java/eu/m724/websocket/WebsocketResource.java
@@ -1,9 +1,12 @@
package eu.m724.websocket;
-import io.smallrye.mutiny.Uni;
+import eu.m724.websocket.packet.DisconnectReason;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
-import jakarta.websocket.*;
+import jakarta.websocket.OnClose;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.OnOpen;
+import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import java.nio.ByteBuffer;
@@ -25,7 +28,11 @@ public class WebsocketResource {
@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()));
+ System.out.printf(
+ "WS [%s]: disconnected, authenticated: %b\n",
+ session.getId(),
+ websocketService.isAuthenticated(session.getId())
+ );
}
@OnMessage
@@ -36,13 +43,29 @@ public class WebsocketResource {
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
+
+ if (success) {
+ websocketService.pong(session);
+ } else { // wrong key
+ websocketService.disconnect(
+ session,
+ DisconnectReason.UNAUTHORIZED,
+ "Invalid access key"
+ );
+ }
+ } else { // wrong version
+ websocketService.disconnect(
+ session,
+ DisconnectReason.VERSION_MISMATCH,
+ "Expected %d, got %d".formatted(websocketService.protocolVersion, clientVersion)
+ );
}
}
return;
@@ -50,19 +73,13 @@ public class WebsocketResource {
switch (message[0]) {
case 0x00:
- pong(session);
+ websocketService.pong(session);
+ break;
+ case 0x01:
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 -> {
@@ -71,6 +88,6 @@ public class WebsocketResource {
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
index 1aa54a0..76db035 100644
--- a/src/main/java/eu/m724/websocket/WebsocketService.java
+++ b/src/main/java/eu/m724/websocket/WebsocketService.java
@@ -2,14 +2,14 @@ package eu.m724.websocket;
import eu.m724.auth.master.AccountService;
import eu.m724.orm.AccessKey;
-import eu.m724.orm.Account;
+import eu.m724.websocket.packet.DisconnectReason;
+import eu.m724.websocket.packet.clientbound.PongPacket;
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.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -21,7 +21,7 @@ public class WebsocketService {
@Inject
AccountService accountService;
- private final Map accounts = new ConcurrentHashMap<>();
+ private final Map accounts = new ConcurrentHashMap<>();
void addSession(String sessionId) {
accounts.put(sessionId, null);
@@ -32,22 +32,18 @@ public class WebsocketService {
}
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 });
- )
+ try {
+ session.close(reason.asCloseReason(message));
+ } catch (IOException ignored) { }
}
boolean authenticate(String sessionId, byte[] bytes) {
- AccessKey accessKey = accountService.findByAccessKey(byte);
+ AccessKey accessKey = accountService.findByAccessKey(bytes);
- if (ac == null)
+ if (accessKey == null)
return false;
- accounts.put(sessionId, account);
+ accounts.put(sessionId, accessKey);
return true;
}
@@ -55,29 +51,7 @@ public class WebsocketService {
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;
- }
+ void pong(Session session) {
+ new PongPacket().send(session);
}
}
diff --git a/src/main/java/eu/m724/websocket/packet/ArgumentsPacket.java b/src/main/java/eu/m724/websocket/packet/ArgumentsPacket.java
new file mode 100644
index 0000000..2868ccb
--- /dev/null
+++ b/src/main/java/eu/m724/websocket/packet/ArgumentsPacket.java
@@ -0,0 +1,31 @@
+package eu.m724.websocket.packet;
+
+import java.nio.ByteBuffer;
+
+public abstract class ArgumentsPacket> implements Packet {
+ private final byte packetId;
+ private final ByteBuffer byteBuffer;
+ private final Object[] arguments;
+
+ public ArgumentsPacket(byte packetId, Object... arguments) {
+ this.packetId = packetId;
+ this.arguments = arguments;
+ this.byteBuffer = compose(packetId);
+ }
+
+ @Override
+ public byte packetId() {
+ return packetId;
+ }
+
+ @Override
+ public Object[] arguments() {
+ return arguments;
+ }
+
+ public ByteBuffer byteBuffer() {
+ return byteBuffer;
+ }
+
+ public abstract ByteBuffer compose(byte packetId, Object... arguments);
+}
diff --git a/src/main/java/eu/m724/websocket/packet/DisconnectReason.java b/src/main/java/eu/m724/websocket/packet/DisconnectReason.java
new file mode 100644
index 0000000..03a7921
--- /dev/null
+++ b/src/main/java/eu/m724/websocket/packet/DisconnectReason.java
@@ -0,0 +1,25 @@
+package eu.m724.websocket.packet;
+
+import jakarta.websocket.CloseReason;
+
+public enum DisconnectReason {
+ UNAUTHORIZED((byte)3001),
+ VERSION_MISMATCH((byte)3002),
+ TIMEOUT((byte)3003),
+ ACCESS_KEY_REVOKED((byte)3004),
+ SERVER_ERROR((byte)3005);
+
+ public final int code;
+
+ DisconnectReason(int code) {
+ this.code = code;
+ }
+
+ public CloseReason.CloseCode closeCode() {
+ return CloseReason.CloseCodes.getCloseCode(code);
+ }
+
+ public CloseReason asCloseReason(String message) {
+ return new CloseReason(closeCode(), message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/websocket/packet/EmptyPacket.java b/src/main/java/eu/m724/websocket/packet/EmptyPacket.java
index 38e508f..60d0550 100644
--- a/src/main/java/eu/m724/websocket/packet/EmptyPacket.java
+++ b/src/main/java/eu/m724/websocket/packet/EmptyPacket.java
@@ -2,9 +2,9 @@ package eu.m724.websocket.packet;
import java.nio.ByteBuffer;
-public class EmptyPacket implements Packet {
+public class EmptyPacket> implements Packet {
private final byte packetId;
- public final ByteBuffer byteBuffer;
+ private final ByteBuffer byteBuffer;
public EmptyPacket(byte packetId) {
this.packetId = packetId;
@@ -17,7 +17,12 @@ public class EmptyPacket implements Packet {
}
@Override
- public ByteBuffer compose() {
+ public ByteBuffer byteBuffer() {
return byteBuffer;
}
+
+ @Override
+ public T read(ByteBuffer buffer) {
+ return null;
+ }
}
diff --git a/src/main/java/eu/m724/websocket/packet/Packet.java b/src/main/java/eu/m724/websocket/packet/Packet.java
index 96451be..85df3e1 100644
--- a/src/main/java/eu/m724/websocket/packet/Packet.java
+++ b/src/main/java/eu/m724/websocket/packet/Packet.java
@@ -1,16 +1,20 @@
package eu.m724.websocket.packet;
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.List;
+import jakarta.websocket.Session;
-public interface Packet {
+import java.nio.ByteBuffer;
+
+public interface Packet> {
byte packetId();
- default List