work for a bit
This commit is contained in:
parent
2542e3f05e
commit
f7f2d781bc
8 changed files with 268 additions and 1 deletions
48
PROTOCOL.md
Normal file
48
PROTOCOL.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
The first message from the server is:
|
||||||
|
- `0x6d 0x73`
|
||||||
|
|
||||||
|
The client should reply, in a single message:
|
||||||
|
1. `0xb6 0xc4`
|
||||||
|
2. client version (byte), right now it's 0
|
||||||
|
3. length of access key (byte)
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## Disconnect reasons
|
||||||
|
- `0x00` - unspecified
|
||||||
|
- `0x01` - incompatible client
|
||||||
|
- `0x02` - timeout (client didn't send ping)
|
||||||
|
- `0x03` - access key revoked
|
||||||
|
- `0x04` - server error
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
- `0x00` - not used
|
4
pom.xml
4
pom.xml
|
@ -46,6 +46,10 @@
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-websockets</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -29,6 +29,22 @@ public class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets an access key
|
||||||
|
* @param bytes access key as bytes
|
||||||
|
* @return the {@link AccessKey} if correct else null even if key is null
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AccessKey findByAccessKey(byte[] bytes) {
|
||||||
|
if (bytes == null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return AccessKey.find("key", (Object) bytes).firstResult();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO maybe move some of these methods somewhere else and reconsider making them static
|
// TODO maybe move some of these methods somewhere else and reconsider making them static
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
76
src/main/java/eu/m724/websocket/WebsocketResource.java
Normal file
76
src/main/java/eu/m724/websocket/WebsocketResource.java
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package eu.m724.websocket;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.websocket.*;
|
||||||
|
import jakarta.websocket.server.ServerEndpoint;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
@ServerEndpoint("/api/ws")
|
||||||
|
public class WebsocketResource {
|
||||||
|
@Inject
|
||||||
|
WebsocketService websocketService;
|
||||||
|
|
||||||
|
@OnOpen
|
||||||
|
public void onOpen(Session session) {
|
||||||
|
websocketService.addSession(session.getId());
|
||||||
|
System.out.printf("WS [%s]: connected\n", session.getId());
|
||||||
|
|
||||||
|
session.getAsyncRemote().sendBinary(ByteBuffer.wrap(new byte[] { 0x6d, 0x73 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnMessage
|
||||||
|
public void onMessage(byte[] message, Session session) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
ByteBuffer command = ByteBuffer.wrap(message);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message[0]) {
|
||||||
|
case 0x00:
|
||||||
|
pong(session);
|
||||||
|
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 -> {
|
||||||
|
s.getAsyncRemote().sendObject(message, result -> {
|
||||||
|
if (result.getException() != null) {
|
||||||
|
System.out.println("Unable to send message: " + result.getException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
}
|
83
src/main/java/eu/m724/websocket/WebsocketService.java
Normal file
83
src/main/java/eu/m724/websocket/WebsocketService.java
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package eu.m724.websocket;
|
||||||
|
|
||||||
|
import eu.m724.auth.master.AccountService;
|
||||||
|
import eu.m724.orm.AccessKey;
|
||||||
|
import eu.m724.orm.Account;
|
||||||
|
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.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class WebsocketService {
|
||||||
|
@ConfigProperty(name = "rwws.protocol_version")
|
||||||
|
byte protocolVersion;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AccountService accountService;
|
||||||
|
|
||||||
|
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
void addSession(String sessionId) {
|
||||||
|
accounts.put(sessionId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeConnection(String sessionId) {
|
||||||
|
accounts.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean authenticate(String sessionId, byte[] bytes) {
|
||||||
|
AccessKey accessKey = accountService.findByAccessKey(byte);
|
||||||
|
|
||||||
|
if (ac == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
accounts.put(sessionId, account);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAuthenticated(String sessionId) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/main/java/eu/m724/websocket/packet/EmptyPacket.java
Normal file
23
src/main/java/eu/m724/websocket/packet/EmptyPacket.java
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package eu.m724.websocket.packet;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class EmptyPacket implements Packet {
|
||||||
|
private final byte packetId;
|
||||||
|
public final ByteBuffer byteBuffer;
|
||||||
|
|
||||||
|
public EmptyPacket(byte packetId) {
|
||||||
|
this.packetId = packetId;
|
||||||
|
this.byteBuffer = ByteBuffer.wrap(new byte[] { packetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte packetId() {
|
||||||
|
return packetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer compose() {
|
||||||
|
return byteBuffer;
|
||||||
|
}
|
||||||
|
}
|
16
src/main/java/eu/m724/websocket/packet/Packet.java
Normal file
16
src/main/java/eu/m724/websocket/packet/Packet.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package eu.m724.websocket.packet;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Packet {
|
||||||
|
byte packetId();
|
||||||
|
|
||||||
|
default List<Object> arguments() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer compose();
|
||||||
|
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
quarkus.datasource.db.kind=h2
|
quarkus.datasource.db.kind=h2
|
||||||
|
|
||||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||||
|
|
||||||
|
rwws.protocol_version=0
|
Loading…
Reference in a new issue