progress
This commit is contained in:
parent
f7f2d781bc
commit
eec5450159
9 changed files with 182 additions and 90 deletions
53
PROTOCOL.md
53
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<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 |
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
});*/
|
||||
}
|
||||
});*
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
31
src/main/java/eu/m724/websocket/packet/ArgumentsPacket.java
Normal file
31
src/main/java/eu/m724/websocket/packet/ArgumentsPacket.java
Normal 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);
|
||||
}
|
25
src/main/java/eu/m724/websocket/packet/DisconnectReason.java
Normal file
25
src/main/java/eu/m724/websocket/packet/DisconnectReason.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue