Система вложенных тегов (ECI), связь с базой данных

This commit is contained in:
RoyceDa
2026-02-02 05:13:32 +02:00
parent d1be8e7014
commit 14a8615746
24 changed files with 326 additions and 59 deletions

View File

@@ -0,0 +1,7 @@
package com.rosetta.im;
public class Configuration {
public static final int PROTOCOL_VERSION = 1;
}

View File

@@ -0,0 +1,26 @@
package com.rosetta.im;
import com.rosetta.im.protocol.BaseFailures;
public enum Failures implements BaseFailures {
/**
* Код ошибки, указывающий на неизвестную ошибку.
*/
UNSUPPORTED_PROTOCOL(3008);
private final int code;
Failures(int code) {
this.code = code;
}
/**
* Получает код ошибки.
* @return Код ошибки.
*/
public int getCode() {
return code;
}
}

View File

@@ -22,7 +22,7 @@ public class Main {
/**
* Запуск сервера на порту 8881
*/
Server server = new Server(settings, manager);
Server server = new Server(settings, manager, Configuration.class);
server.start();
}
}

View File

@@ -0,0 +1,68 @@
package com.rosetta.im.client.tags;
import com.rosetta.im.packet.enums.HandshakeStage;
import com.rosetta.im.protocol.ECITag;
/**
* Это вложенный обьект для клиента, содержащий информацию об аутентификации.
*/
public class Authentificate extends ECITag {
public Device device;
public String publicKey;
public String privateKey;
public HandshakeStage handshakeStage;
public Authentificate() {
super("authentificate");
}
public Authentificate(Device device, String publicKey, String privateKey, HandshakeStage handshakeStage) {
super("authentificate");
this.device = device;
this.publicKey = publicKey;
this.privateKey = privateKey;
this.handshakeStage = handshakeStage;
}
public Device getDevice() {
return device;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public HandshakeStage getHandshakeStage() {
return handshakeStage;
}
public void setDevice(Device device) {
this.device = device;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public void setHandshakeStage(HandshakeStage handshakeStage) {
this.handshakeStage = handshakeStage;
}
/**
* Проверяет, прошел ли клиент аутентификацию. В том числе подтвердил ли устройство.
* @return true если аутентификация пройдена, иначе false.
*/
public boolean hasAuthorized() {
return this.handshakeStage == HandshakeStage.COMPLETED;
}
}

View File

@@ -1,4 +1,4 @@
package com.rosetta.im.device;
package com.rosetta.im.client.tags;
public class Device {

View File

@@ -3,6 +3,7 @@ package com.rosetta.im.database;
import org.hibernate.Session;
import org.hibernate.Transaction;
import java.util.HashMap;
import java.util.List;
public class DatabaseManager {
@@ -61,6 +62,28 @@ public class DatabaseManager {
}
}
public static <T> T findBy(Class<T> entity, HashMap<String, Object> params) {
Session session = HibernateUtil.openSession();
try {
StringBuilder queryString = new StringBuilder("FROM " + entity.getSimpleName() + " WHERE ");
int index = 0;
for (String key : params.keySet()) {
if (index > 0) {
queryString.append(" AND ");
}
queryString.append(key).append(" = :").append(key);
index++;
}
var query = session.createQuery(queryString.toString(), entity);
for (var entry : params.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
return query.uniqueResult();
} finally {
session.close();
}
}
public static <T> void delete(T entity) {
Session session = HibernateUtil.openSession();
Transaction transaction = null;

View File

@@ -43,9 +43,6 @@ public class User extends BaseEntity {
@Column(name = "publicKey", nullable = false, unique = true)
private String publicKey;
@Column(name = "timestamp", nullable = false)
private long timestamp;
@Column(name = "createdTime", nullable = false, updatable = false)
private Date createdTime;
@@ -104,14 +101,6 @@ public class User extends BaseEntity {
this.publicKey = publicKey;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public Date getCreatedTime() {
return createdTime;
}

View File

@@ -1,18 +1,90 @@
package com.rosetta.im.executors;
import java.util.HashMap;
import com.rosetta.im.Configuration;
import com.rosetta.im.Failures;
import com.rosetta.im.client.tags.Authentificate;
import com.rosetta.im.client.tags.Device;
import com.rosetta.im.database.DatabaseManager;
import com.rosetta.im.database.entity.User;
import com.rosetta.im.packet.Packet0Handshake;
import com.rosetta.im.packet.enums.HandshakeStage;
import com.rosetta.im.protocol.Client;
import com.rosetta.im.protocol.packet.Packet;
import com.rosetta.im.protocol.packet.PacketExecutor;
public class Executor0Handshake implements PacketExecutor {
public class Executor0Handshake extends PacketExecutor {
@Override
public void onPacketReceived(Packet packet, Client client) {
Packet0Handshake handshake = (Packet0Handshake) packet;
String publicKey = handshake.getPublicKey();
String privateKey = handshake.getPrivateKey();
int b = 0;
Device device = handshake.getDevice();
int protocolVersion = handshake.getProtocolVersion();
/**
* Получаем информацию об аутентификации клиента
* используя возможности ECI тэгов.
*/
Authentificate authentificate = client.getTag(new Authentificate());
if(authentificate != null && authentificate.hasAuthorized()) {
/**
* Клиент уже авторизован, повторный хэндшейк не допускается
*/
return;
}
/**
* Проверяем корректность версии протокола
*/
if(protocolVersion != Configuration.PROTOCOL_VERSION) {
client.disconnect(Failures.UNSUPPORTED_PROTOCOL);
return;
}
/**
* Проверяем есть ли такой пользователь
*/
User user = DatabaseManager.findBy(User.class, new HashMap<>() {{
put("publicKey", publicKey);
}});
if(user == null) {
/**
* Пользователь не найден, создаем нового
*/
user = new User();
user.setPrivateKey(privateKey);
user.setPublicKey(publicKey);
user.setUsername("");
user.setTitle(publicKey.substring(0, 7));
/**
* Новый пользователь не верифицирован
*/
user.setVerified(0);
DatabaseManager.save(user);
Authentificate eciTag = new Authentificate(device, publicKey, privateKey, HandshakeStage.COMPLETED);
client.addTag(eciTag);
System.out.println("New user created: " + user.getId());
Authentificate authTag = client.getTag(new Authentificate());
System.out.println("eciTag" + authTag);
return;
}
}
}

View File

@@ -1,6 +1,6 @@
package com.rosetta.im.packet;
import com.rosetta.im.device.Device;
import com.rosetta.im.client.tags.Device;
import com.rosetta.im.packet.enums.HandshakeStage;
import com.rosetta.im.protocol.Stream;
import com.rosetta.im.protocol.packet.Packet;

View File

@@ -0,0 +1,9 @@
package com.rosetta.im.protocol;
public interface BaseFailures {
/**
* Получает код ошибки.
* @return
*/
int getCode();
}

View File

@@ -1,7 +1,7 @@
package com.rosetta.im.protocol;
import java.util.HashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import org.java_websocket.WebSocket;
@@ -14,7 +14,10 @@ public class Client {
public WebSocket socket;
public String clientId;
public Map<String, Object> clientData;
/**
* Любые данные, связанные с клиентом.
*/
public Set<ECITag> eciTags;
/**
* Интервал отправки heartbeat пакетов в миллисекундах.
*/
@@ -33,7 +36,7 @@ public class Client {
public Client(WebSocket socket, long heartbeatInterval) {
this.socket = socket;
this.clientId = StringUtil.randomString(32);
this.clientData = new HashMap<>();
this.eciTags = new HashSet<ECITag>();
this.heartbeatInterval = heartbeatInterval;
this.lastHeartbeatTime = System.currentTimeMillis();
}
@@ -68,8 +71,8 @@ public class Client {
* Получает данные, связанные с клиентом.
* @return Данные клиента.
*/
public Map<String, Object> getClientData() {
return clientData;
public Set<ECITag> getEciTags() {
return this.eciTags;
}
/**
@@ -85,16 +88,16 @@ public class Client {
* @param key Ключ данных.
* @param value Значение данных.
*/
public void setData(String key, Object value) {
this.clientData.put(key, value);
public <T extends ECITag> void addTag(T eciTag) {
this.eciTags.add(eciTag);
}
/**
* Устанавливает данные клиента.
* @param data Данные клиента.
*/
public void setData(Map<String, Object> data) {
this.clientData = data;
public void setTags(Set<ECITag> eciTags) {
this.eciTags = eciTags;
}
/**
@@ -102,8 +105,27 @@ public class Client {
* @param key Ключ данных.
* @return Значение данных.
*/
public Object getData(String key) {
return this.clientData.get(key);
public <T extends ECITag> T getTag(ECITag eciTag) {
for (ECITag tag : this.eciTags) {
if (tag.getKey().equals(eciTag.getKey())) {
return (T) tag;
}
}
return null;
}
/**
* Проверяет наличие данных клиента по указанному ключу.
* @param key Ключ данных.
* @return true если данные существуют, иначе false.
*/
public boolean hasTag(ECITag eciTag) {
for (ECITag tag : this.eciTags) {
if (tag.getKey().equals(eciTag.getKey())) {
return true;
}
}
return false;
}
/**
@@ -125,7 +147,7 @@ public class Client {
* Отключает клиента с указанным кодом отказа.
* @param code Код отказа.
*/
public void disconnect(Failures code) {
public void disconnect(BaseFailures code) {
this.disconnect(code.getCode());
}
@@ -133,7 +155,7 @@ public class Client {
* Отключает клиента с неизвестной причиной.
*/
public void disconnect() {
this.disconnect(Failures.UNKNOWN_FAILURE);
this.disconnect(ServerFailures.UNKNOWN_FAILURE);
}
}

View File

@@ -0,0 +1,31 @@
package com.rosetta.im.protocol;
/**
* Embedded Client Information Tag.
*
* Используется для хранения дополнительной информации о клиенте.
*/
public abstract class ECITag {
/**
* Ключ тега.
*/
public String key;
/**
* Создает новый тег с указанным ключом и значением.
* @param key Ключ тега.
*/
public ECITag(String key) {
this.key = key;
}
/**
* Получает ключ тега.
* @return Ключ тега.
*/
public String getKey() {
return this.key;
}
}

View File

@@ -19,11 +19,13 @@ public class Server extends WebSocketServer {
private PacketManager packetManager;
private Settings settings;
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Object attachment;
public Server(Settings settings, PacketManager packetManager) {
public Server(Settings settings, PacketManager packetManager, Object attachment) {
super(new InetSocketAddress(settings.port));
this.settings = settings;
this.packetManager = packetManager;
this.attachment = attachment;
}
@Override
@@ -62,14 +64,14 @@ public class Server extends WebSocketServer {
/**
* Если пакет не поддерживается, отключаем клиента с соответствующим кодом ошибки.
*/
client.disconnect(Failures.UNSUPPORTED_PACKET);
client.disconnect(ServerFailures.UNSUPPORTED_PACKET);
return;
}
if(!this.packetManager.hasExecutorDelegated(packetId)){
/**
* Если для пакета не назначен обработчик, отключаем клиента с соответствующим кодом ошибки.
*/
client.disconnect(Failures.UNSUPPORTED_PACKET);
client.disconnect(ServerFailures.UNSUPPORTED_PACKET);
return;
}
Class<? extends Packet> packetClass = this.packetManager.getPacketClass(packetId);
@@ -86,6 +88,8 @@ public class Server extends WebSocketServer {
*/
Class<? extends PacketExecutor> executorClass = this.packetManager.getExecutors().get(packetId);
PacketExecutor executor = executorClass.getConstructor().newInstance();
executor.settings = this.settings;
executor.attachment = this.attachment;
executor.onPacketReceived(packet, client);
} catch (Exception e) {
System.out.println("Error while processing packet " + packetClass.getName());
@@ -124,7 +128,7 @@ public class Server extends WebSocketServer {
for(WebSocket socket : this.getConnections()) {
Client client = socket.getAttachment();
if(!client.isAlive()) {
client.disconnect(Failures.INACTIVITY_TIMEOUT);
client.disconnect(ServerFailures.INACTIVITY_TIMEOUT);
}
}
}, this.settings.heartbeatInterval, this.settings.heartbeatInterval, TimeUnit.MILLISECONDS);

View File

@@ -3,7 +3,7 @@ package com.rosetta.im.protocol;
/**
* Перечисление кодов ошибок, используемых в протоколе.
*/
public enum Failures {
public enum ServerFailures implements BaseFailures {
/**
* Код ошибки, указывающий на несоответствие данных.
*/
@@ -40,7 +40,7 @@ public enum Failures {
private final int code;
Failures(int code) {
ServerFailures(int code) {
this.code = code;
}

View File

@@ -1,7 +1,35 @@
package com.rosetta.im.protocol.packet;
import com.rosetta.im.protocol.Client;
import com.rosetta.im.protocol.Settings;
public interface PacketExecutor {
public void onPacketReceived(Packet packet, Client client);
/**
* Базовый класс для обработчиков пакетов.
*/
public abstract class PacketExecutor {
public Settings settings;
public Object attachment;
/**
* Настройки сервера.
* @return
*/
public Settings getSettings() {
return settings;
}
/**
* Вложенный обьект, который был передан при создании сервера.
* @return вложенный обьект
*/
public Object getAttachment() {
return attachment;
}
/**
* Вызывается при получении пакета от клиента.
* @param packet Пакет, полученный от клиента.
* @param client Клиент, отправивший пакет.
*/
public abstract void onPacketReceived(Packet packet, Client client);
}

View File

@@ -3,10 +3,10 @@
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
<property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/rosetta_db</property>
<property name="hibernate.connection.username">rosetta_user</property>
<property name="hibernate.connection.password">rosetta_password</property>
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
<property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/your_database</property>
<property name="hibernate.connection.username">your_user</property>
<property name="hibernate.connection.password">your_password</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>