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
|
## Format
|
||||||
Packet id then arguments \
|
Packet id then arguments \
|
||||||
Some packets don't have arguments so send just the packet id \
|
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
|
## Authentication
|
||||||
The first message from the server is:
|
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
|
4. access key, decoded from base64
|
||||||
|
|
||||||
Authentication complete, the server will send a disconnect if something's wrong, otherwise it will pong. \
|
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
|
# Commands
|
||||||
### Client -> Server
|
### Server bound (Client -> Server)
|
||||||
- `0x00` - Ping
|
| Code | Name | Data | Notes |
|
||||||
* no body
|
|--------|----------|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
* the server replies with 0x00 Pong
|
| `0x00` | Ping | | the server replies with pong<br/>must be sent by client every at most 30 seconds, otherwise the server disconnects |
|
||||||
* also a keepalive, sent by client in at most 30 second intervals 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 |
|
||||||
- `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
|
### Client bound (Server -> Client)
|
||||||
- `0x00` - Pong
|
|
||||||
* the body is a single signed long (8 bytes) which the current unix time
|
| Code | Name | Data | Notes |
|
||||||
* a response to client's command of the same id
|
|--------|------|----------------------|----------------------------------------------------------------------|
|
||||||
|
| `0x00` | Pong | 1. long: unix millis | A response to ping, also sent by server on successful authentication |
|
||||||
|
|
||||||
## Disconnect reasons
|
## Disconnect reasons
|
||||||
- `0x00` - unspecified
|
On every disconnect there's a human-readable message the client should display
|
||||||
- `0x01` - incompatible client
|
|
||||||
- `0x02` - timeout (client didn't send ping)
|
| Code | Name | Notes |
|
||||||
- `0x03` - access key revoked
|
|--------|--------------------|-------------------------------------------------|
|
||||||
- `0x04` - server error
|
| `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
|
## 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
|
# 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
|
### Usage
|
||||||
See `RUNNING.md`[^1]
|
See `RUNNING.md`[^1]
|
||||||
|
|
||||||
|
### Websocket message format
|
||||||
|
See `PROTOCOL.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[^1]: This is the default Quarkus readme that I wish was available online
|
[^1]: This is the default Quarkus readme that I wish was available online
|
|
@ -1,9 +1,12 @@
|
||||||
package eu.m724.websocket;
|
package eu.m724.websocket;
|
||||||
|
|
||||||
import io.smallrye.mutiny.Uni;
|
import eu.m724.websocket.packet.DisconnectReason;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
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 jakarta.websocket.server.ServerEndpoint;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
@ -25,7 +28,11 @@ public class WebsocketResource {
|
||||||
@OnClose
|
@OnClose
|
||||||
public void onClose(Session session) {
|
public void onClose(Session session) {
|
||||||
websocketService.removeConnection(session.getId());
|
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
|
@OnMessage
|
||||||
|
@ -36,13 +43,29 @@ public class WebsocketResource {
|
||||||
if (!websocketService.isAuthenticated(session.getId())) {
|
if (!websocketService.isAuthenticated(session.getId())) {
|
||||||
if (command.get() == (byte)0xb6 && command.get() == (byte)0xc4) {
|
if (command.get() == (byte)0xb6 && command.get() == (byte)0xc4) {
|
||||||
byte clientVersion = command.get();
|
byte clientVersion = command.get();
|
||||||
|
|
||||||
if (clientVersion == websocketService.protocolVersion) {
|
if (clientVersion == websocketService.protocolVersion) {
|
||||||
byte keyLength = command.get();
|
byte keyLength = command.get();
|
||||||
byte[] accessKey = new byte[keyLength];
|
byte[] accessKey = new byte[keyLength];
|
||||||
command.get(accessKey);
|
command.get(accessKey);
|
||||||
|
|
||||||
boolean success = websocketService.authenticate(sessionId, 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;
|
return;
|
||||||
|
@ -50,19 +73,13 @@ public class WebsocketResource {
|
||||||
|
|
||||||
switch (message[0]) {
|
switch (message[0]) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
pong(session);
|
websocketService.pong(session);
|
||||||
|
break;
|
||||||
|
case 0x01:
|
||||||
break;
|
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) {
|
/*private void broadcast(String message) {
|
||||||
sessions.values().forEach(s -> {
|
sessions.values().forEach(s -> {
|
||||||
|
@ -71,6 +88,6 @@ public class WebsocketResource {
|
||||||
System.out.println("Unable to send message: " + result.getException());
|
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.auth.master.AccountService;
|
||||||
import eu.m724.orm.AccessKey;
|
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.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.websocket.Session;
|
import jakarta.websocket.Session;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ public class WebsocketService {
|
||||||
@Inject
|
@Inject
|
||||||
AccountService accountService;
|
AccountService accountService;
|
||||||
|
|
||||||
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
|
private final Map<String, AccessKey> accounts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
void addSession(String sessionId) {
|
void addSession(String sessionId) {
|
||||||
accounts.put(sessionId, null);
|
accounts.put(sessionId, null);
|
||||||
|
@ -32,22 +32,18 @@ public class WebsocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnect(Session session, DisconnectReason reason, String message) {
|
void disconnect(Session session, DisconnectReason reason, String message) {
|
||||||
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
try {
|
||||||
ByteBuffer byteBuffer = ByteBuffer.allocate(3 + messageBytes.length);
|
session.close(reason.asCloseReason(message));
|
||||||
byteBuffer.put(0x0)
|
} catch (IOException ignored) { }
|
||||||
|
|
||||||
session.getAsyncRemote().sendBinary(
|
|
||||||
ByteBuffer.wrap(new byte[] { reason });
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean authenticate(String sessionId, byte[] bytes) {
|
boolean authenticate(String sessionId, byte[] bytes) {
|
||||||
AccessKey accessKey = accountService.findByAccessKey(byte);
|
AccessKey accessKey = accountService.findByAccessKey(bytes);
|
||||||
|
|
||||||
if (ac == null)
|
if (accessKey == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
accounts.put(sessionId, account);
|
accounts.put(sessionId, accessKey);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,29 +51,7 @@ public class WebsocketService {
|
||||||
return accounts.containsKey(sessionId);
|
return accounts.containsKey(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Packet {
|
void pong(Session session) {
|
||||||
PING((byte)0x00),
|
new PongPacket().send(session);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public class EmptyPacket implements Packet {
|
public class EmptyPacket<T extends EmptyPacket<T>> implements Packet<T> {
|
||||||
private final byte packetId;
|
private final byte packetId;
|
||||||
public final ByteBuffer byteBuffer;
|
private final ByteBuffer byteBuffer;
|
||||||
|
|
||||||
public EmptyPacket(byte packetId) {
|
public EmptyPacket(byte packetId) {
|
||||||
this.packetId = packetId;
|
this.packetId = packetId;
|
||||||
|
@ -17,7 +17,12 @@ public class EmptyPacket implements Packet {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer compose() {
|
public ByteBuffer byteBuffer() {
|
||||||
return byteBuffer;
|
return byteBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(ByteBuffer buffer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
package eu.m724.websocket.packet;
|
package eu.m724.websocket.packet;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import jakarta.websocket.Session;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface Packet {
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public interface Packet<T extends Packet<T>> {
|
||||||
byte packetId();
|
byte packetId();
|
||||||
|
|
||||||
default List<Object> arguments() {
|
default Object[] arguments() {
|
||||||
return Collections.emptyList();
|
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