This commit is contained in:
Minecon724 2024-08-21 18:01:37 +02:00
parent f7f2d781bc
commit eec5450159
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
9 changed files with 182 additions and 90 deletions

View file

@ -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<br/>must be sent by client every at most 30 seconds, otherwise the server disconnects |
| `0x01` | Settings | 1. byte: setting id<br/>2. value<br/>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
| Code | Name | Notes |
|--------|------------|-------------------------------------------------|
| `0x00` | reserved | |
| `0xFF` | terminator | not a setting, just used to terminate sequences |

View file

@ -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

View file

@ -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());
}
});
});*/
}
});*
}*/
}

View file

@ -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<String, Account> accounts = new ConcurrentHashMap<>();
private final Map<String, AccessKey> 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);
}
}

View file

@ -0,0 +1,31 @@
package eu.m724.websocket.packet;
import java.nio.ByteBuffer;
public abstract class ArgumentsPacket<T extends ArgumentsPacket<T>> implements Packet<T> {
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);
}

View file

@ -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);
}
}

View file

@ -2,9 +2,9 @@ package eu.m724.websocket.packet;
import java.nio.ByteBuffer;
public class EmptyPacket implements Packet {
public class EmptyPacket<T extends EmptyPacket<T>> implements Packet<T> {
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;
}
}

View file

@ -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<T extends Packet<T>> {
byte packetId();
default List<Object> 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());
}
}

View file

@ -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<PongPacket> {
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());
}
}