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 arguments() { - return Collections.emptyList(); + default Object[] arguments() { + return new Object[0]; } - ByteBuffer compose(); + ByteBuffer byteBuffer(); + T read(ByteBuffer buffer); + default void send(Session session) { // TODO should this be here? + session.getAsyncRemote().sendBinary(byteBuffer()); + } } diff --git a/src/main/java/eu/m724/websocket/packet/clientbound/PongPacket.java b/src/main/java/eu/m724/websocket/packet/clientbound/PongPacket.java new file mode 100644 index 0000000..3981161 --- /dev/null +++ b/src/main/java/eu/m724/websocket/packet/clientbound/PongPacket.java @@ -0,0 +1,30 @@ +package eu.m724.websocket.packet.clientbound; + +import eu.m724.websocket.packet.ArgumentsPacket; + +import java.nio.ByteBuffer; + +public class PongPacket extends ArgumentsPacket { + public PongPacket(long time) { + super((byte)0x00, time); + } + + public PongPacket() { + super((byte)0x00, System.currentTimeMillis()); + } + + + @Override + public ByteBuffer compose(byte packetId, Object... arguments) { + ByteBuffer byteBuffer = ByteBuffer.allocate(9); + byteBuffer.put(packetId); + byteBuffer.putLong((long)arguments[0]); + return byteBuffer; + } + + + @Override + public PongPacket read(ByteBuffer buffer) { + return new PongPacket(buffer.getLong()); + } +}