diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f8f0c7b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + ".DS_Store": true, + "._*": true, + "**/._*": true + }, + "java.project.explorer.showNonJavaResources": false, + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..19b9e3e --- /dev/null +++ b/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.rosetta.im + rosetta-server + 1.0-SNAPSHOT + + + 17 + 17 + + + + + + + + + org.java-websocket + Java-WebSocket + 1.6.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/rosetta/im/Main.java b/src/main/java/com/rosetta/im/Main.java new file mode 100644 index 0000000..f09e66a --- /dev/null +++ b/src/main/java/com/rosetta/im/Main.java @@ -0,0 +1,20 @@ +package com.rosetta.im; + + +import com.rosetta.im.packet.Packet0Handshake; +import com.rosetta.im.protocol.Server; +import com.rosetta.im.protocol.packet.PacketManager; + +public class Main { + public static void main(String[] args) { + PacketManager manager = new PacketManager(); + manager.registerPacket(0, Packet0Handshake.class); + + + Server server = new Server(8881, manager); + server.start(); + + + System.out.println("Rosetta server started..."); + } +} \ No newline at end of file diff --git a/src/main/java/com/rosetta/im/device/Device.java b/src/main/java/com/rosetta/im/device/Device.java new file mode 100644 index 0000000..e3ebfb9 --- /dev/null +++ b/src/main/java/com/rosetta/im/device/Device.java @@ -0,0 +1,39 @@ +package com.rosetta.im.device; + +public class Device { + + public String deviceId; + public String deviceName; + public String deviceOs; + + public Device(String deviceId, String deviceName, String deviceOs) { + this.deviceId = deviceId; + this.deviceName = deviceName; + this.deviceOs = deviceOs; + } + + public String getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public String getDeviceOs() { + return deviceOs; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public void setDeviceOs(String deviceOs) { + this.deviceOs = deviceOs; + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor0Handshake.java b/src/main/java/com/rosetta/im/executors/Executor0Handshake.java new file mode 100644 index 0000000..349fab1 --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor0Handshake.java @@ -0,0 +1,13 @@ +package com.rosetta.im.executors; + +import com.rosetta.im.protocol.packet.Packet; +import com.rosetta.im.protocol.packet.PacketExecutor; + +public class Executor0Handshake implements PacketExecutor { + + @Override + public void onPacketReceived(Class packet) { + + } + +} diff --git a/src/main/java/com/rosetta/im/packet/Packet0Handshake.java b/src/main/java/com/rosetta/im/packet/Packet0Handshake.java new file mode 100644 index 0000000..6389fce --- /dev/null +++ b/src/main/java/com/rosetta/im/packet/Packet0Handshake.java @@ -0,0 +1,83 @@ +package com.rosetta.im.packet; + +import com.rosetta.im.device.Device; +import com.rosetta.im.packet.enums.HandshakeStage; +import com.rosetta.im.protocol.Stream; +import com.rosetta.im.protocol.packet.Packet; + +/** + * Пакет хэндшейка между клиентом и сервером. + * Используется для установления соединения и подтверждения от сервера что клиент + * тот за кого себя выдает. + * + * Протокол таблица: + * 0 - packetId (int16) + * 1 - privateKey (string) + * 2 - publicKey (string) + * 3 - protocolVersion (int8) + * 4 - heartbeatInterval (int8) + * 5 - deviceId (string) + * 6 - deviceName (string) + * 7 - deviceOs (string) + * 8 - handshakeStage (int8) + */ +public class Packet0Handshake extends Packet { + /** + * Публичный и приватный ключи клиента + */ + private String publicKey; + private String privateKey; + /** + * Версия протокола клиента + */ + private int protocolVersion = 1; + + /** + * Интервал отправки heartbeat пакетов в секундах + */ + private int heartbeatInterval = 15; + + /** + * Минимальная информация об устройстве клиента + */ + private Device device; + + /** + * Стадия рукопожатия + * 0 - COMPLETED + * 1 - NEED_DEVICE_VERIFICATION + */ + private HandshakeStage handshakeStage = HandshakeStage.COMPLETED; + + + @Override + public Stream write() { + Stream stream = new Stream(); + stream.writeInt16(this.packetId); + stream.writeString(this.privateKey); + stream.writeString(this.publicKey); + stream.writeInt8(this.protocolVersion); + stream.writeInt8(this.heartbeatInterval); + stream.writeString(this.device.getDeviceId()); + stream.writeString(this.device.getDeviceName()); + stream.writeString(this.device.getDeviceOs()); + stream.writeInt8(this.handshakeStage.getCode()); + return stream; + } + + @Override + public void read(Stream stream) { + this.privateKey = stream.readString(); + this.publicKey = stream.readString(); + this.protocolVersion = stream.readInt8(); + this.heartbeatInterval = stream.readInt8(); + String deviceId = stream.readString(); + String deviceName = stream.readString(); + String deviceOs = stream.readString(); + this.device = new Device(deviceId, deviceName, deviceOs); + this.handshakeStage = HandshakeStage.fromCode( + stream.readInt8() + ); + } + +} diff --git a/src/main/java/com/rosetta/im/packet/enums/HandshakeStage.java b/src/main/java/com/rosetta/im/packet/enums/HandshakeStage.java new file mode 100644 index 0000000..a581057 --- /dev/null +++ b/src/main/java/com/rosetta/im/packet/enums/HandshakeStage.java @@ -0,0 +1,36 @@ +package com.rosetta.im.packet.enums; + +/** + * Этапы хэндшейка между клиентом и сервером. + */ +public enum HandshakeStage { + /** + * Успешный хэндшейк + * Такой пользователь может авторизованно взаимодействовать с сервером + */ + COMPLETED(0), + /** + * Необходима верификация устройства + * Такой пользователь должен подтвердить устройство (например, через код на другом устройстве) + */ + NEED_DEVICE_VERIFICATION(1); + + private final int code; + + HandshakeStage(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static HandshakeStage fromCode(int code) { + for (HandshakeStage stage : HandshakeStage.values()) { + if (stage.getCode() == code) { + return stage; + } + } + throw new IllegalArgumentException("Invalid HandshakeStage code: " + code); + } +} diff --git a/src/main/java/com/rosetta/im/protocol/Client.java b/src/main/java/com/rosetta/im/protocol/Client.java new file mode 100644 index 0000000..58b04a0 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/Client.java @@ -0,0 +1,46 @@ +package com.rosetta.im.protocol; + +import java.util.HashMap; +import java.util.Map; + +import org.java_websocket.WebSocket; + +import com.rosetta.im.protocol.util.StringUtil; + +public class Client { + + public WebSocket socket; + public String clientId; + public Map clientData; + + public Client(WebSocket socket) { + this.socket = socket; + this.clientId = StringUtil.randomString(32); + this.clientData = new HashMap<>(); + } + + public String getClientId() { + return clientId; + } + + public Map getClientData() { + return clientData; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public void setData(String key, Object value) { + this.clientData.put(key, value); + } + + public void setData(Map data) { + this.clientData = data; + } + + public Object getData(String key) { + return this.clientData.get(key); + } + +} diff --git a/src/main/java/com/rosetta/im/protocol/Server.java b/src/main/java/com/rosetta/im/protocol/Server.java new file mode 100644 index 0000000..f1bfaf0 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/Server.java @@ -0,0 +1,70 @@ +package com.rosetta.im.protocol; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +import com.rosetta.im.protocol.packet.Packet; +import com.rosetta.im.protocol.packet.PacketManager; + +public class Server extends WebSocketServer { + + private PacketManager packetManager; + + public Server(int port, PacketManager packetManager) { + super(new InetSocketAddress(port)); + this.packetManager = packetManager; + } + + @Override + public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) { + + } + + @Override + public void onError(WebSocket arg0, Exception arg1) { + + } + + @Override + public void onMessage(WebSocket arg0, String arg1) { + + } + + @Override + public void onMessage(WebSocket socket, ByteBuffer byteBuffer) { + Client client = socket.getAttachment(); + + byte[] bytes = byteBuffer.array(); + Stream stream = new Stream(bytes); + int packetId = stream.readInt16(); + Class packetClass = this.packetManager.getPacketClass(packetId); + if(packetClass == null){ + System.out.println("Received unknown packet with id: " + packetId); + return; + } + try { + Packet packet = packetClass.getConstructor().newInstance(); + packet.packetId = packetId; + packet.read(stream); + System.out.println("Received packet: " + packetClass.getSimpleName()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onOpen(WebSocket socket, ClientHandshake arg1) { + Client client = new Client(socket); + socket.setAttachment(client); + } + + @Override + public void onStart() { + + } + +} diff --git a/src/main/java/com/rosetta/im/protocol/Stream.java b/src/main/java/com/rosetta/im/protocol/Stream.java new file mode 100644 index 0000000..b54ee64 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/Stream.java @@ -0,0 +1,179 @@ +package com.rosetta.im.protocol; + +public class Stream { + + private byte[] stream; + private int readPointer = 0; + private int writePointer = 0; + + public Stream() { + this.stream = new byte[0]; + } + + public Stream(byte[] stream) { + this.stream = stream; + } + + public byte[] getStream() { + return this.stream; + } + + public void setStream(byte[] stream) { + this.stream = stream; + } + + public void writeInt8(int value) { + int negationBit = value < 0 ? 1 : 0; + int int8Value = Math.abs(value) & 0xFF; + + ensureCapacity(writePointer >> 3); + stream[writePointer >> 3] |= (byte)(negationBit << (7 - (writePointer & 7))); + writePointer++; + + for (int i = 0; i < 8; i++) { + int bit = (int8Value >> (7 - i)) & 1; + ensureCapacity(writePointer >> 3); + stream[writePointer >> 3] |= (byte)(bit << (7 - (writePointer & 7))); + writePointer++; + } + } + + public int readInt8() { + int value = 0; + int negationBit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; + readPointer++; + + for (int i = 0; i < 8; i++) { + int bit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; + value |= bit << (7 - i); + readPointer++; + } + + return negationBit == 1 ? -value : value; + } + + public void writeBit(int value) { + int bit = value & 1; + ensureCapacity(writePointer >> 3); + stream[writePointer >> 3] |= (byte)(bit << (7 - (writePointer & 7))); + writePointer++; + } + + public int readBit() { + int bit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; + readPointer++; + return bit; + } + + public void writeBoolean(boolean value) { + writeBit(value ? 1 : 0); + } + + public boolean readBoolean() { + return readBit() == 1; + } + + public void writeInt16(int value) { + writeInt8(value >> 8); + writeInt8(value & 0xFF); + } + + public int readInt16() { + int value = readInt8() << 8; + return value | readInt8(); + } + + public void writeInt32(int value) { + writeInt16(value >> 16); + writeInt16(value & 0xFFFF); + } + + public int readInt32() { + int value = readInt16() << 16; + return value | readInt16(); + } + + public void writeInt64(long value) { + int high = (int)(value >> 32); + int low = (int)value; + writeInt32(high); + writeInt32(low); + } + + public long readInt64() { + long high = readInt32(); + long low = readInt32() & 0xFFFFFFFFL; + return (high << 32) | low; + } + + public void writeFloat32(float value) { + int floatValue = Float.floatToIntBits(value); + writeInt32(floatValue); + } + + public float readFloat32() { + int floatValue = readInt32(); + return Float.intBitsToFloat(floatValue); + } + + public void writeString(String value) { + int length = value.length(); + writeInt32(length); + for (int i = 0; i < value.length(); i++) { + writeInt16(value.charAt(i)); + } + } + + public String readString() { + int length = readInt32(); + + // Security fix for string length exceeding stream capacity + if (length < 0 || length > (stream.length - (readPointer >> 3))) { + System.out.println("Stream readString length invalid: " + length + ", stream length: " + stream.length + ", readPointer: " + readPointer); + return ""; + } + + StringBuilder value = new StringBuilder(); + for (int i = 0; i < length; i++) { + value.append((char)readInt16()); + } + return value.toString(); + } + + public void writeBytes(byte[] value) { + writeInt32(value.length); + for (int i = 0; i < value.length; i++) { + writeInt8(value[i]); + } + } + + public byte[] readBytes() { + int length = readInt32(); + byte[] value = new byte[length]; + for (int i = 0; i < length; i++) { + value[i] = (byte)readInt8(); + } + return value; + } + + public boolean isEmpty() { + return this.stream.length == 0; + } + + public int length() { + return this.stream.length; + } + + public byte[] getBuffer() { + return this.stream; + } + + private void ensureCapacity(int byteIndex) { + if (byteIndex >= stream.length) { + byte[] newStream = new byte[byteIndex + 1]; + System.arraycopy(stream, 0, newStream, 0, stream.length); + stream = newStream; + } + } + +} diff --git a/src/main/java/com/rosetta/im/protocol/packet/Packet.java b/src/main/java/com/rosetta/im/protocol/packet/Packet.java new file mode 100644 index 0000000..bb74db9 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/packet/Packet.java @@ -0,0 +1,31 @@ +package com.rosetta.im.protocol.packet; + +import com.rosetta.im.protocol.Stream; + +/** + * Представляет собой абстрактный класс для всех пакетов, используемых в протоколе. + */ +public abstract class Packet { + + public int packetId; + public PacketManager packetManager; + + public Packet() { + + } + + /** + * Записывает данные пакета в поток. Исползуется при отправке + * + * @return Поток с записанными данными пакета. + */ + public abstract Stream write(); + + /** + * Читает данные пакета из потока. Используется при получении + * + * @param stream Поток с данными пакета. + */ + public abstract void read(Stream stream); + +} diff --git a/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java b/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java new file mode 100644 index 0000000..bca1a0a --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java @@ -0,0 +1,5 @@ +package com.rosetta.im.protocol.packet; + +public interface PacketExecutor { + public void onPacketReceived(Class packet); +} diff --git a/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java b/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java new file mode 100644 index 0000000..cabd5dd --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java @@ -0,0 +1,35 @@ +package com.rosetta.im.protocol.packet; + +import java.util.HashMap; + +public class PacketManager { + + private HashMap> packets; + + public PacketManager() { + this.packets = new HashMap<>(); + } + + public void registerPacket(int packetId, Class packet) { + this.packets.put(packetId, packet); + } + + public boolean isPacketRegistred(Packet packet) { + return this.packets.containsValue(packet.getClass()); + } + + public Integer getPacketIdByClass(Class packetClass) { + for (var entry : this.packets.entrySet()) { + if (entry.getValue().equals(packetClass)) { + return entry.getKey(); + } + } + return null; + } + + public Class getPacketClass(int packetId) { + return this.packets.get(packetId); + } + + +} diff --git a/src/main/java/com/rosetta/im/protocol/util/StringUtil.java b/src/main/java/com/rosetta/im/protocol/util/StringUtil.java new file mode 100644 index 0000000..877f270 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/util/StringUtil.java @@ -0,0 +1,15 @@ +package com.rosetta.im.protocol.util; + +public class StringUtil { + + public static String randomString(int length) { + StringBuilder sb = new StringBuilder(); + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; i < length; i++) { + int index = (int) (Math.random() * characters.length()); + sb.append(characters.charAt(index)); + } + return sb.toString(); + } + +} diff --git a/target/classes/com/rosetta/im/Main.class b/target/classes/com/rosetta/im/Main.class new file mode 100644 index 0000000..33955d8 Binary files /dev/null and b/target/classes/com/rosetta/im/Main.class differ diff --git a/target/classes/com/rosetta/im/device/Device.class b/target/classes/com/rosetta/im/device/Device.class new file mode 100644 index 0000000..f6959d7 Binary files /dev/null and b/target/classes/com/rosetta/im/device/Device.class differ diff --git a/target/classes/com/rosetta/im/executors/Executor0Handshake.class b/target/classes/com/rosetta/im/executors/Executor0Handshake.class new file mode 100644 index 0000000..1b3aead Binary files /dev/null and b/target/classes/com/rosetta/im/executors/Executor0Handshake.class differ diff --git a/target/classes/com/rosetta/im/packet/Packet0Handshake.class b/target/classes/com/rosetta/im/packet/Packet0Handshake.class new file mode 100644 index 0000000..20b0d02 Binary files /dev/null and b/target/classes/com/rosetta/im/packet/Packet0Handshake.class differ diff --git a/target/classes/com/rosetta/im/packet/enums/HandshakeStage.class b/target/classes/com/rosetta/im/packet/enums/HandshakeStage.class new file mode 100644 index 0000000..4bec1d7 Binary files /dev/null and b/target/classes/com/rosetta/im/packet/enums/HandshakeStage.class differ diff --git a/target/classes/com/rosetta/im/protocol/Client.class b/target/classes/com/rosetta/im/protocol/Client.class new file mode 100644 index 0000000..0a642fb Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/Client.class differ diff --git a/target/classes/com/rosetta/im/protocol/Server.class b/target/classes/com/rosetta/im/protocol/Server.class new file mode 100644 index 0000000..ca113bb Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/Server.class differ diff --git a/target/classes/com/rosetta/im/protocol/Stream.class b/target/classes/com/rosetta/im/protocol/Stream.class new file mode 100644 index 0000000..b023c77 Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/Stream.class differ diff --git a/target/classes/com/rosetta/im/protocol/packet/Packet.class b/target/classes/com/rosetta/im/protocol/packet/Packet.class new file mode 100644 index 0000000..d6c377d Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/packet/Packet.class differ diff --git a/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class b/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class new file mode 100644 index 0000000..a4c2f31 Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class differ diff --git a/target/classes/com/rosetta/im/protocol/packet/PacketManager.class b/target/classes/com/rosetta/im/protocol/packet/PacketManager.class new file mode 100644 index 0000000..972ceaf Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/packet/PacketManager.class differ diff --git a/target/classes/com/rosetta/im/protocol/util/StringUtil.class b/target/classes/com/rosetta/im/protocol/util/StringUtil.class new file mode 100644 index 0000000..14ca2b2 Binary files /dev/null and b/target/classes/com/rosetta/im/protocol/util/StringUtil.class differ