work for a bit

This commit is contained in:
Minecon724 2024-08-20 18:09:20 +02:00
parent 2542e3f05e
commit f7f2d781bc
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
8 changed files with 268 additions and 1 deletions

48
PROTOCOL.md Normal file
View 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

View file

@ -46,6 +46,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
</dependency>
</dependencies>
<build>

View file

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

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

View 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;
}
}
}

View 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;
}
}

View 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();
}

View file

@ -1,3 +1,4 @@
quarkus.datasource.db.kind=h2
quarkus.hibernate-orm.database.generation=drop-and-create
rwws.protocol_version=0