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>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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
|
||||
|
||||
/**
|
||||
|
|
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.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